feat(ui): 校正首页发现详情播放器预览

This commit is contained in:
Zhou Weidong committed 2026-10-19 15:50:11 +08:00
1 parent 87db548591
commit 21d457fd98
13 files changed
+1218 -1684

No files matched your search

+158 -203
View File
@@ -13,7 +13,6 @@ import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/app_bottom_nav.dart';
import '../../core/widgets/empty_state.dart'; import '../../core/widgets/empty_state.dart';
import '../../core/widgets/glass_panel.dart';
import '../../core/widgets/remote_image.dart'; import '../../core/widgets/remote_image.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import 'discover_binding.dart'; import 'discover_binding.dart';
@@ -165,53 +164,41 @@ class _DiscoverPageState extends State<DiscoverPage> {
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 12), padding: const EdgeInsets.fromLTRB(14, 2, 14, 10),
child: Row( child: Row(
children: [ children: [
Container(
width: 3,
height: 18,
decoration: BoxDecoration(
color: AppColors.blue,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 6),
Expanded( Expanded(
child: Column( child: Text(
crossAxisAlignment: CrossAxisAlignment.start, l10n.trendingMicroDramas,
children: [ maxLines: 1,
Text( overflow: TextOverflow.ellipsis,
l10n.trendingMicroDramas, style: const TextStyle(
style: const TextStyle( color: AppColors.onSurface,
fontFamily: 'Outfit', fontSize: AppDimens.previewSectionTitle,
color: AppColors.onSurface, fontWeight: FontWeight.w800,
fontSize: 17, ),
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
l10n.trendingSubtitle,
style: const TextStyle(
color: AppColors.subtle,
fontSize: 11,
),
),
],
), ),
), ),
TextButton( TextButton(
onPressed: () => onPressed: () =>
categories.selectCode(CategoryController.hotCode), categories.selectCode(CategoryController.allCode),
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: AppColors.blue, foregroundColor: AppColors.accent,
minimumSize: const Size(44, 44), minimumSize: const Size(44, 44),
padding: const EdgeInsets.symmetric(horizontal: 8), padding: EdgeInsets.zero,
), ),
child: Row( child: Text(
mainAxisSize: MainAxisSize.min, '${l10n.all} ›',
children: [ style: const TextStyle(fontSize: 10),
Text(
l10n.seeAll,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const Icon(Icons.chevron_right_rounded, size: 16),
],
), ),
), ),
], ],
@@ -236,55 +223,41 @@ class _DiscoverPageState extends State<DiscoverPage> {
Widget _header(AppLocalizations l10n) { Widget _header(AppLocalizations l10n) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 12), padding: const EdgeInsets.fromLTRB(14, 2, 14, 10),
child: Row( child: Row(
children: [ children: [
Expanded( Container(
child: Column( width: 24,
crossAxisAlignment: CrossAxisAlignment.start, height: 24,
children: [ alignment: Alignment.center,
Row( decoration: BoxDecoration(
children: [ color: AppColors.blue,
Container( borderRadius: BorderRadius.circular(7),
width: 8, ),
height: 8, child: const Icon(
decoration: const BoxDecoration( Icons.play_arrow_rounded,
color: AppColors.blue, color: AppColors.onPrimary,
shape: BoxShape.circle, size: 16,
),
),
const SizedBox(width: 8),
Flexible(
child: Text(
l10n.appName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.onSurface,
fontSize: 21,
fontWeight: FontWeight.w700,
height: 1.05,
letterSpacing: 0,
),
),
),
],
),
const SizedBox(height: 3),
Text(
l10n.verticalMiniSeries,
style: const TextStyle(
color: AppColors.muted,
fontSize: 10,
fontWeight: FontWeight.w500,
letterSpacing: 0.6,
),
),
],
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 8),
Text(
l10n.discoverTab,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.onSurface,
fontSize: 15,
fontWeight: FontWeight.w800,
),
),
const Spacer(),
_HeaderIcon(
key: const Key('discover-search-button'),
icon: Icons.search_rounded,
tooltip: l10n.search,
onTap: logic.openSearch,
),
const SizedBox(width: 7),
Semantics( Semantics(
button: true, button: true,
label: widget.signedIn ? l10n.myTab : l10n.logIn, label: widget.signedIn ? l10n.myTab : l10n.logIn,
@@ -293,20 +266,20 @@ class _DiscoverPageState extends State<DiscoverPage> {
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
onTap: widget.signedIn ? widget.onCoins : widget.onSignIn, onTap: widget.signedIn ? widget.onCoins : widget.onSignIn,
child: Container( child: Container(
width: 38, width: 28,
height: 38, height: 28,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: AppColors.raised, gradient: const LinearGradient(
border: Border.all( begin: Alignment.topLeft,
color: AppColors.secondary.withValues(alpha: 0.7), end: Alignment.bottomRight,
width: 2, colors: [Color(0xFF6A5CFF), AppColors.blue],
), ),
), ),
child: const Icon( child: const Icon(
Icons.person_rounded, Icons.person_rounded,
color: AppColors.onSurface, color: AppColors.onSurface,
size: 21, size: 15,
), ),
), ),
), ),
@@ -318,9 +291,9 @@ class _DiscoverPageState extends State<DiscoverPage> {
Widget _search() { Widget _search() {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 14),
child: SizedBox( child: SizedBox(
height: AppDimens.searchHeight, height: 36,
child: TextField( child: TextField(
key: const Key('discover-search'), key: const Key('discover-search'),
controller: logic.search, controller: logic.search,
@@ -331,27 +304,22 @@ class _DiscoverPageState extends State<DiscoverPage> {
decoration: InputDecoration( decoration: InputDecoration(
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: AppColors.card, fillColor: AppColors.input,
hintText: AppLocalizations.of(context)!.searchDiscover, hintText: AppLocalizations.of(context)!.searchDiscover,
hintStyle: const TextStyle(color: AppColors.subtle, fontSize: 12.5), hintStyle: const TextStyle(color: AppColors.cream, fontSize: 11),
prefixIcon: const Icon( prefixIcon: const Icon(
Icons.search_rounded, Icons.search_rounded,
color: AppColors.subtle, color: AppColors.subtle,
size: 20, size: 17,
),
suffixIcon: const Icon(
Icons.mic_none_rounded,
color: AppColors.muted,
size: 19,
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 12),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimens.glassSmallRadius), borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(color: AppColors.border), borderSide: BorderSide.none,
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimens.glassSmallRadius), borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(color: AppColors.blue, width: 1.2), borderSide: const BorderSide(color: AppColors.blue, width: 1),
), ),
), ),
), ),
@@ -361,9 +329,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
Widget _categories(AppLocalizations l10n, CategoryController categories) { Widget _categories(AppLocalizations l10n, CategoryController categories) {
final choices = <({String code, String label})>[ final choices = <({String code, String label})>[
(code: CategoryController.allCode, label: l10n.all), (code: CategoryController.allCode, label: l10n.recommended),
(code: CategoryController.hotCode, label: l10n.hot),
(code: CategoryController.newCode, label: l10n.newest),
for (final category in categories.categories) for (final category in categories.categories)
(code: 'category-${category.id}', label: category.name), (code: 'category-${category.id}', label: category.name),
]; ];
@@ -371,7 +337,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
key: const Key('home-category-chips'), key: const Key('home-category-chips'),
height: AppDimens.tapTarget, height: AppDimens.tapTarget,
child: ListView.separated( child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 14),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: choices.length, itemCount: choices.length,
separatorBuilder: (_, _) => const SizedBox(width: 8), separatorBuilder: (_, _) => const SizedBox(width: 8),
@@ -390,12 +356,10 @@ class _DiscoverPageState extends State<DiscoverPage> {
alignment: Alignment.center, alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? AppColors.blue : AppColors.card, color: selected ? AppColors.blue : const Color(0xFF29272B),
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
border: Border.all( border: Border.all(
color: selected color: selected ? AppColors.blue : Colors.transparent,
? AppColors.blue
: Colors.white.withValues(alpha: 0.08),
), ),
), ),
child: Text( child: Text(
@@ -403,7 +367,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
color: selected ? AppColors.onPrimary : AppColors.muted, color: selected ? AppColors.onPrimary : AppColors.muted,
fontSize: 12, fontSize: AppDimens.chipFont,
fontWeight: selected ? FontWeight.w700 : FontWeight.w500, fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
height: 1.1, height: 1.1,
), ),
@@ -423,23 +387,20 @@ class _DiscoverPageState extends State<DiscoverPage> {
WatchProgress record, WatchProgress record,
) { ) {
final progress = record.fraction; final progress = record.fraction;
return GlassPanel( return Container(
key: const Key('discover-continue-watching'), key: const Key('discover-continue-watching'),
radius: AppDimens.glassSmallRadius, padding: const EdgeInsets.all(9),
blur: 18, decoration: BoxDecoration(
padding: const EdgeInsets.all(12), color: AppColors.cardAlt,
gradient: const LinearGradient( borderRadius: BorderRadius.circular(12),
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0x1FFFFFFF), Color(0x0AFFFFFF)],
), ),
child: Row( child: Row(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(10),
child: SizedBox( child: SizedBox(
width: 64, width: 52,
height: 88, height: 66,
child: RemoteImage( child: RemoteImage(
drama.posterUrl, drama.posterUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
@@ -447,57 +408,38 @@ class _DiscoverPageState extends State<DiscoverPage> {
), ),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 9),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
children: [ l10n.continueWatching,
Container( maxLines: 1,
width: 6, overflow: TextOverflow.ellipsis,
height: 6, style: const TextStyle(
decoration: const BoxDecoration( color: AppColors.accent,
color: AppColors.blue, fontSize: 8,
shape: BoxShape.circle, fontWeight: FontWeight.w800,
), ),
),
const SizedBox(width: 5),
Flexible(
child: Text(
l10n.continueWatching,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.blue,
fontSize: 9.5,
fontWeight: FontWeight.w700,
letterSpacing: 0.4,
),
),
),
],
), ),
const SizedBox(height: 3), const SizedBox(height: 2),
Text( Text(
drama.title, drama.title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: AppColors.onSurface, color: AppColors.onSurface,
fontSize: 13, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w800,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
_resumeMeta(drama, record), _resumeShort(drama, record),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 9),
color: AppColors.muted,
fontSize: 10.5,
),
), ),
const SizedBox(height: 7), const SizedBox(height: 7),
ClipRRect( ClipRRect(
@@ -505,7 +447,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: progress <= 0 ? null : progress, value: progress <= 0 ? null : progress,
minHeight: 3, minHeight: 3,
backgroundColor: Colors.white.withValues(alpha: 0.12), backgroundColor: const Color(0xFF474248),
valueColor: const AlwaysStoppedAnimation(AppColors.blue), valueColor: const AlwaysStoppedAnimation(AppColors.blue),
), ),
), ),
@@ -525,29 +467,24 @@ class _DiscoverPageState extends State<DiscoverPage> {
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: AppColors.blue, backgroundColor: AppColors.blue,
foregroundColor: AppColors.onPrimary, foregroundColor: AppColors.onPrimary,
minimumSize: const Size(64, 34), minimumSize: const Size(56, 28),
padding: const EdgeInsets.symmetric(horizontal: 10), padding: EdgeInsets.zero,
shape: const StadiumBorder(), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(9),
),
textStyle: const TextStyle( textStyle: const TextStyle(
fontSize: 11, fontSize: 10,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
child: Row( child: Text(l10n.resume),
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.play),
const SizedBox(width: 2),
const Icon(Icons.play_arrow_rounded, size: 14),
],
),
), ),
], ],
), ),
); );
} }
String _resumeMeta(Drama drama, WatchProgress record) { String _resumeShort(Drama drama, WatchProgress record) {
final label = AppLocalizations.of(context)!; final label = AppLocalizations.of(context)!;
Episode? episode; Episode? episode;
for (final item in drama.episodes) { for (final item in drama.episodes) {
@@ -556,10 +493,8 @@ class _DiscoverPageState extends State<DiscoverPage> {
break; break;
} }
} }
final head = episode == null if (episode == null) return _duration(record.position);
? '' return label.episodeOfTotal(episode.number, drama.episodeCount);
: '${label.episodeLabel(episode.number)} / ${drama.episodeCount} · ';
return '$head${_duration(record.position)} / ${_duration(record.duration)}';
} }
Widget _quickActions(AppLocalizations l10n, CategoryController categories) { Widget _quickActions(AppLocalizations l10n, CategoryController categories) {
@@ -574,7 +509,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
onTap: logic.openSearch, onTap: logic.openSearch,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 7),
Expanded( Expanded(
child: _QuickAction( child: _QuickAction(
key: const Key('discover-quick-charts'), key: const Key('discover-quick-charts'),
@@ -584,17 +519,17 @@ class _DiscoverPageState extends State<DiscoverPage> {
onTap: () => categories.selectCode(CategoryController.hotCode), onTap: () => categories.selectCode(CategoryController.hotCode),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 7),
Expanded( Expanded(
child: _QuickAction( child: _QuickAction(
key: const Key('discover-quick-new'), key: const Key('discover-quick-new'),
icon: Icons.auto_awesome_rounded, icon: Icons.auto_awesome_rounded,
label: l10n.newest, label: l10n.newReleases,
color: AppColors.gold, color: AppColors.gold,
onTap: () => categories.selectCode(CategoryController.newCode), onTap: () => categories.selectCode(CategoryController.newCode),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 7),
Expanded( Expanded(
child: _QuickAction( child: _QuickAction(
key: const Key('discover-quick-watchlist'), key: const Key('discover-quick-watchlist'),
@@ -617,8 +552,8 @@ class _DiscoverPageState extends State<DiscoverPage> {
items: [ items: [
AppBottomNavItem( AppBottomNavItem(
tabKey: const Key('discover-nav-home'), tabKey: const Key('discover-nav-home'),
icon: Icons.home_outlined, icon: Icons.play_circle_outline_rounded,
selectedIcon: Icons.home_rounded, selectedIcon: Icons.play_circle_fill_rounded,
label: l10n.homeTab, label: l10n.homeTab,
onTap: () => logic.act( onTap: () => logic.act(
context, context,
@@ -637,8 +572,8 @@ class _DiscoverPageState extends State<DiscoverPage> {
), ),
AppBottomNavItem( AppBottomNavItem(
tabKey: const Key('discover-nav-my'), tabKey: const Key('discover-nav-my'),
icon: Icons.person_outline_rounded, icon: Icons.video_library_outlined,
selectedIcon: Icons.person_rounded, selectedIcon: Icons.video_library_rounded,
label: l10n.myTab, label: l10n.myTab,
onTap: () => logic.act( onTap: () => logic.act(
context, context,
@@ -651,6 +586,37 @@ class _DiscoverPageState extends State<DiscoverPage> {
); );
} }
class _HeaderIcon extends StatelessWidget {
const _HeaderIcon({
super.key,
required this.icon,
required this.tooltip,
required this.onTap,
});
final IconData icon;
final String tooltip;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 28,
height: 28,
child: IconButton(
tooltip: tooltip,
onPressed: onTap,
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2C2D33),
foregroundColor: AppColors.onSurface,
),
icon: Icon(icon, size: 14),
),
);
}
}
class _QuickAction extends StatelessWidget { class _QuickAction extends StatelessWidget {
const _QuickAction({ const _QuickAction({
super.key, super.key,
@@ -668,31 +634,20 @@ class _QuickAction extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: AppColors.card, color: AppColors.quick,
borderRadius: BorderRadius.circular(AppDimens.glassSmallRadius), borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
height: 76, height: 62,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)),
borderRadius: BorderRadius.circular(AppDimens.glassSmallRadius),
border: Border.all(color: AppColors.border),
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( Icon(icon, color: color, size: 16),
width: 32, const SizedBox(height: 4),
height: 32,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 17),
),
const SizedBox(height: 5),
Text( Text(
label, label,
maxLines: 2, maxLines: 2,
@@ -700,7 +655,7 @@ class _QuickAction extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: AppColors.muted, color: AppColors.muted,
fontSize: 9.5, fontSize: 9,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
height: 1.05, height: 1.05,
), ),
+55 -102
View File
@@ -37,27 +37,22 @@ class _HomeDramaGrid extends StatelessWidget {
), ),
); );
} }
if (dramas.isEmpty) { if (dramas.isEmpty) {
final missingCategory = categories.selectedSectionMissing;
return _fill( return _fill(
EmptyState( EmptyState(
key: const Key('home-all-dramas-empty'), key: const Key('home-all-dramas-empty'),
icon: searching icon: searching
? Icons.search_off_rounded ? Icons.search_off_rounded
: missingCategory
? Icons.grid_view_rounded
: Icons.movie_filter_outlined, : Icons.movie_filter_outlined,
title: searching title: searching
? l10n.noSearchResults ? l10n.noSearchResults
: missingCategory : categories.selectedSectionMissing
? l10n.noCategoryDramas ? l10n.noCategoryDramas
: l10n.noSearchResults, : l10n.noSearchResults,
subtitle: l10n.emptyHint, subtitle: l10n.emptyHint,
), ),
); );
} }
return SliverMainAxisGroup( return SliverMainAxisGroup(
slivers: [ slivers: [
_dramaGrid(dramas), _dramaGrid(dramas),
@@ -68,14 +63,14 @@ class _HomeDramaGrid extends StatelessWidget {
Widget _dramaGrid(List<Drama> items) => SliverPadding( Widget _dramaGrid(List<Drama> items) => SliverPadding(
key: const Key('home-all-dramas-section'), key: const Key('home-all-dramas-section'),
padding: const EdgeInsets.symmetric(horizontal: AppDimens.pageHorizontal), padding: const EdgeInsets.symmetric(horizontal: 14),
sliver: SliverGrid( sliver: SliverGrid(
key: const Key('home-all-dramas-grid'), key: const Key('home-all-dramas-grid'),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, crossAxisCount: 2,
mainAxisSpacing: 14, mainAxisSpacing: 10,
crossAxisSpacing: 12, crossAxisSpacing: 10,
childAspectRatio: 0.55, mainAxisExtent: 164,
), ),
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, index) => _HomeDramaCard( (context, index) => _HomeDramaCard(
@@ -88,9 +83,8 @@ class _HomeDramaGrid extends StatelessWidget {
), ),
); );
Widget _fill(Widget child) { Widget _fill(Widget child) =>
return SliverFillRemaining(hasScrollBody: false, child: child); SliverFillRemaining(hasScrollBody: false, child: child);
}
} }
class _HomeDramaCard extends StatelessWidget { class _HomeDramaCard extends StatelessWidget {
@@ -107,18 +101,21 @@ class _HomeDramaCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final genre = drama.genre.isEmpty ? drama.category : drama.genre; final genre = drama.genre.isEmpty ? drama.category : drama.genre;
final free =
drama.freeEpisodes > 0 ||
drama.episodes.any((episode) => episode.isFree);
return InkWell( return InkWell(
key: Key('home-drama-card-${drama.id}'), key: Key('home-drama-card-${drama.id}'),
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(AppDimens.cardRadius), borderRadius: BorderRadius.circular(10),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AspectRatio( SizedBox(
key: Key('home-drama-cover-${drama.id}'), key: Key('home-drama-cover-${drama.id}'),
aspectRatio: 3 / 4.2, height: 120,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(AppDimens.cardRadius), borderRadius: BorderRadius.circular(10),
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@@ -132,120 +129,76 @@ class _HomeDramaCard extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [Color(0x0D000000), Color(0xCC000000)],
Color(0x33000000), stops: [0.45, 1],
Color(0x00000000),
Color(0xB3000000),
],
stops: [0, 0.42, 1],
), ),
), ),
), ),
Positioned( if (free)
top: 8,
left: 8,
child: Wrap(
spacing: 4,
runSpacing: 4,
children: [
if (drama.isHot) _badge(l10n.hot, AppColors.blue),
if (drama.freeEpisodes > 0)
_badge(l10n.free, AppColors.success),
],
),
),
if (drama.viewCount > 0)
Positioned( Positioned(
right: 8, top: 6,
bottom: 8, left: 6,
child: DecoratedBox( child: Container(
decoration: BoxDecoration( padding: const EdgeInsets.symmetric(
color: Colors.black.withValues(alpha: 0.42), horizontal: 6,
borderRadius: BorderRadius.circular(7), vertical: 2,
), ),
child: Padding( decoration: BoxDecoration(
padding: const EdgeInsets.symmetric( color: AppColors.blue,
horizontal: 6, borderRadius: BorderRadius.circular(6),
vertical: 2, ),
), child: Text(
child: Text( l10n.free,
_compactCount(drama.viewCount), style: const TextStyle(
key: Key('home-drama-views-${drama.id}'), color: AppColors.onPrimary,
style: const TextStyle( fontSize: 8,
color: AppColors.cream, fontWeight: FontWeight.w800,
fontSize: 10,
fontWeight: FontWeight.w600,
),
), ),
), ),
), ),
), ),
Positioned(
left: 6,
right: 6,
bottom: 6,
child: Text(
drama.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onPrimary,
fontSize: 9,
fontWeight: FontWeight.w800,
shadows: [Shadow(blurRadius: 6, color: Colors.black)],
),
),
),
], ],
), ),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 5),
Text( Text(
drama.title, drama.title,
key: Key('home-drama-title-${drama.id}'), key: Key('home-drama-title-${drama.id}'),
maxLines: 2, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: AppColors.onSurface, color: AppColors.onSurface,
fontSize: 13, fontSize: 11,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w800,
height: 1.2,
), ),
), ),
const SizedBox(height: 3), const SizedBox(height: 2),
Text( Text(
genre.isEmpty ? l10n.videoPending : genre, genre.isEmpty ? l10n.videoPending : genre,
key: Key('home-drama-genre-${drama.id}'), key: Key('home-drama-genre-${drama.id}'),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 9),
color: AppColors.subtle,
fontSize: 11,
fontWeight: FontWeight.w400,
height: 1.2,
),
), ),
], ],
), ),
); );
} }
Widget _badge(String label, Color color) {
return DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(999),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
child: Text(
label,
style: const TextStyle(
color: AppColors.onPrimary,
fontSize: 9,
fontWeight: FontWeight.w800,
letterSpacing: 0.3,
height: 1.1,
),
),
),
);
}
}
String _compactCount(int value) {
if (value >= 1000000) {
final scaled = value / 1000000;
return '${scaled.toStringAsFixed(scaled >= 10 ? 0 : 1)}M';
}
if (value >= 1000) {
final scaled = value / 1000;
return '${scaled.toStringAsFixed(scaled >= 10 ? 0 : 1)}K';
}
return '$value';
} }
File diff suppressed because it is too large. Load diff
+12
View File
@@ -531,6 +531,18 @@ class PlayerLogic extends ChangeNotifier {
: null; : null;
} }
bool get hasNextEpisode => nextVisibleIndex(session.currentIndex) != null;
Future<void> playNextEpisode() async {
final next = nextVisibleIndex(session.currentIndex);
if (next == null || !pages.hasClients) return;
await pages.animateToPage(
visibleIndices.indexOf(next),
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
);
}
void beginScrub(VideoPlayerPort player) { void beginScrub(VideoPlayerPort player) {
if (session.active != player) return; if (session.active != player) return;
scrubbingPlayer = player; scrubbingPlayer = player;
+308 -209
View File
@@ -18,13 +18,13 @@ import '../../core/formatting/localized_content.dart';
import '../../core/platform/haptics.dart'; import '../../core/platform/haptics.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_bottom_nav.dart';
import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/download_action.dart'; import '../../core/widgets/download_action.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import 'player_binding.dart'; import 'player_binding.dart';
import 'player_logic.dart'; import 'player_logic.dart';
import 'widgets/home_feed_overlay.dart'; import 'widgets/home_feed_overlay.dart';
import 'widgets/player_featured_comment.dart';
import 'widgets/seek_progress_bar.dart'; import 'widgets/seek_progress_bar.dart';
import '../../core/data/repositories/watch_history_repository.dart'; import '../../core/data/repositories/watch_history_repository.dart';
import '../../core/data/repositories/playback_repository.dart'; import '../../core/data/repositories/playback_repository.dart';
@@ -340,7 +340,6 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
l10n: l10n, l10n: l10n,
drama: drama, drama: drama,
episode: episode, episode: episode,
controller: ready ? videoPort!.controller : null,
liked: interactions.isLiked(drama.id), liked: interactions.isLiked(drama.id),
saved: context.watch<FavoriteController>().isSaved(drama.id), saved: context.watch<FavoriteController>().isSaved(drama.id),
onLike: () async { onLike: () async {
@@ -381,10 +380,6 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
onPip: logic.pictureInPicture.isPlatformSupported onPip: logic.pictureInPicture.isPlatformSupported
? () => unawaited(logic.enterPip(context)) ? () => unawaited(logic.enterPip(context))
: null, : null,
onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null,
onSeek: ready
? (position) => logic.seekTo(context, videoPort!, position)
: null,
) )
else if (!logic.screenCleared) else if (!logic.screenCleared)
_PlaybackOverlay( _PlaybackOverlay(
@@ -398,9 +393,13 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
onBack: widget.embedded onBack: widget.embedded
? null ? null
: () => unawaited(logic.leave(context)), : () => unawaited(logic.leave(context)),
onDiscover: () => logic.showDiscover(context),
onEpisodes: () => logic.showEpisodes(context), onEpisodes: () => logic.showEpisodes(context),
onMy: () => logic.showMy(context), onNext: logic.hasNextEpisode
? () => unawaited(logic.playNextEpisode())
: null,
playing: logic.session.userWantsPlay,
onTogglePlay: () =>
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
// 收藏/点赞要对到具体账号:未登录先引导登录(首页/详情仍可匿名浏览) // 收藏/点赞要对到具体账号:未登录先引导登录(首页/详情仍可匿名浏览)
onLike: () async { onLike: () async {
if (await ensureSignedIn(context)) { if (await ensureSignedIn(context)) {
@@ -434,7 +433,6 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
onDownload: () => requestEpisodeDownload(context, episode.id), onDownload: () => requestEpisodeDownload(context, episode.id),
onComments: () => logic.showComments(context, drama), onComments: () => logic.showComments(context, drama),
onShare: () => logic.shareDrama(context, drama), onShare: () => logic.shareDrama(context, drama),
onClear: () => logic.setScreenCleared(true),
onPip: logic.pictureInPicture.isPlatformSupported onPip: logic.pictureInPicture.isPlatformSupported
? () => unawaited(logic.enterPip(context)) ? () => unawaited(logic.enterPip(context))
: null, : null,
@@ -442,7 +440,6 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
onSeek: ready onSeek: ready
? (position) => logic.seekTo(context, videoPort!, position) ? (position) => logic.seekTo(context, videoPort!, position)
: null, : null,
showBottomNav: false,
), ),
if (logic.screenCleared) if (logic.screenCleared)
SafeArea( SafeArea(
@@ -649,20 +646,19 @@ class _PlaybackOverlay extends StatelessWidget {
required this.controller, required this.controller,
required this.liked, required this.liked,
required this.saved, required this.saved,
required this.playing,
required this.onTogglePlay,
required this.onBack, required this.onBack,
required this.onDiscover,
required this.onEpisodes, required this.onEpisodes,
required this.onMy, required this.onComments,
required this.onLike, required this.onLike,
required this.onSave, required this.onSave,
required this.onDownload, required this.onDownload,
required this.onComments,
required this.onShare, required this.onShare,
required this.onClear,
required this.onPip, required this.onPip,
required this.onScrubStart, required this.onScrubStart,
required this.onSeek, required this.onSeek,
required this.showBottomNav, this.onNext,
}); });
final AppLocalizations l10n; final AppLocalizations l10n;
@@ -671,230 +667,333 @@ class _PlaybackOverlay extends StatelessWidget {
final VideoPlayerController? controller; final VideoPlayerController? controller;
final bool liked; final bool liked;
final bool saved; final bool saved;
final bool playing;
final VoidCallback onTogglePlay;
final VoidCallback? onBack; final VoidCallback? onBack;
final VoidCallback onDiscover;
final VoidCallback onEpisodes; final VoidCallback onEpisodes;
final VoidCallback onMy; final VoidCallback onComments;
final VoidCallback? onNext;
final VoidCallback onLike; final VoidCallback onLike;
final VoidCallback onSave; final VoidCallback onSave;
final VoidCallback onDownload; final VoidCallback onDownload;
final VoidCallback onComments;
final VoidCallback onShare; final VoidCallback onShare;
final VoidCallback onClear;
final VoidCallback? onPip; final VoidCallback? onPip;
final VoidCallback? onScrubStart; final VoidCallback? onScrubStart;
final Future<void> Function(Duration)? onSeek; final Future<void> Function(Duration)? onSeek;
final bool showBottomNav;
@override @override
Widget build(BuildContext context) => SafeArea( Widget build(BuildContext context) {
bottom: showBottomNav, return SafeArea(
child: Stack( child: Column(
children: [ children: [
Column( _topBar(context),
Expanded(
child: Stack(
children: [
Center(
child: GestureDetector(
key: const Key('player-play-toggle'),
onTap: onTogglePlay,
child: Container(
width: 52,
height: 52,
decoration: const BoxDecoration(
color: Color(0x80000000),
shape: BoxShape.circle,
),
child: Icon(
playing
? Icons.pause_rounded
: Icons.play_arrow_rounded,
color: AppColors.onPrimary,
size: 26,
),
),
),
),
const Positioned(
right: 10,
bottom: 14,
child: Text(
'1.0x',
style: TextStyle(color: Color(0xFFDDDDDD), fontSize: 9),
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
child: Column(
children: [
SeekProgressBar(
valueListenable: controller,
semanticsLabel: l10n.seekVideo,
showTimes: false,
onScrubStart: onScrubStart,
onSeek: onSeek,
),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: _PlayerAction(
key: const Key('player-episodes'),
icon: Icons.view_list_rounded,
label: l10n.episodes,
onTap: onEpisodes,
),
),
const SizedBox(width: 8),
Expanded(
child: _PlayerAction(
key: const Key('player-comments'),
icon: Icons.chat_bubble_outline_rounded,
label: l10n.comments,
onTap: onComments,
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 32,
child: FilledButton(
key: const Key('player-next'),
onPressed: onNext,
style: FilledButton.styleFrom(
backgroundColor: AppColors.blue,
disabledBackgroundColor: AppColors.raised,
disabledForegroundColor: AppColors.subtle,
foregroundColor: AppColors.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(9),
),
padding: EdgeInsets.zero,
textStyle: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
l10n.nextEpisode,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 2),
const Icon(Icons.skip_next_rounded, size: 14),
],
),
),
),
),
],
),
const SizedBox(height: 10),
PlayerFeaturedComment(drama: drama, onViewAll: onComments),
],
),
),
],
),
);
}
Widget _topBar(BuildContext context) {
return SizedBox(
height: 46,
child: Row(
children: [
if (onBack != null)
_TopAction(
key: const Key('player-back'),
tooltip: l10n.backToSeries,
icon: Icons.chevron_left_rounded,
onPressed: onBack!,
)
else
const SizedBox(width: 28),
const SizedBox(width: 8),
Expanded(
child: Text(
'${localizedDramaTitle(l10n, drama)} · '
'${l10n.episodeLabel(episode.number)}',
key: const Key('player-title'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.onSurface,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 8),
_TopAction(
key: const Key('player-more'),
tooltip: l10n.more,
icon: Icons.more_horiz_rounded,
onPressed: () => _openMore(context),
),
],
),
);
}
Future<void> _openMore(BuildContext context) async {
await showModalBottomSheet<void>(
context: context,
backgroundColor: AppColors.card,
showDragHandle: true,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Spacer(), ListTile(
Padding( key: const Key('player-more-download'),
padding: const EdgeInsets.fromLTRB(20, 0, 8, 0), leading: const Icon(
child: Row( Icons.download_outlined,
crossAxisAlignment: CrossAxisAlignment.end, color: AppColors.secondary,
children: [
Expanded(child: _info()),
_rail(),
],
), ),
title: Text(l10n.downloads),
onTap: () {
Navigator.pop(sheetContext);
onDownload();
},
), ),
const SizedBox(height: 4), if (onPip != null)
Padding( ListTile(
padding: EdgeInsets.fromLTRB( key: const Key('player-more-pip'),
16, leading: const Icon(
0, Icons.picture_in_picture_alt_outlined,
16, color: AppColors.secondary,
20 + MediaQuery.paddingOf(context).bottom, ),
title: Text(l10n.pictureInPicture),
onTap: () {
Navigator.pop(sheetContext);
onPip!();
},
), ),
child: SeekProgressBar( ListTile(
valueListenable: controller, key: const Key('player-more-like'),
semanticsLabel: l10n.seekVideo, leading: Icon(
onScrubStart: onScrubStart, liked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
onSeek: onSeek, color: liked ? AppColors.blue : AppColors.muted,
), ),
title: Text(l10n.like),
onTap: () {
Navigator.pop(sheetContext);
onLike();
},
),
ListTile(
key: const Key('player-more-favorite'),
leading: Icon(
saved ? Icons.star_rounded : Icons.star_border_rounded,
color: saved ? AppColors.blue : AppColors.muted,
),
title: Text(l10n.favorite),
onTap: () {
Navigator.pop(sheetContext);
onSave();
},
),
ListTile(
key: const Key('player-more-share'),
leading: const Icon(Icons.share_outlined, color: AppColors.muted),
title: Text(l10n.share),
onTap: () {
Navigator.pop(sheetContext);
onShare();
},
), ),
if (showBottomNav) _fallbackNav(),
], ],
), ),
if (onBack != null)
Positioned(
top: 4,
left: 8,
child: IconButton(
tooltip: l10n.backToSeries,
onPressed: onBack,
style: IconButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: const Color(0x59000000),
),
icon: const Icon(Icons.arrow_back_rounded),
),
),
Positioned(
top: 4,
right: 8,
child: IconButton(
tooltip: l10n.clearScreen,
onPressed: onClear,
style: IconButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: const Color(0x59000000),
),
icon: const Icon(Icons.fit_screen_outlined),
),
),
],
),
);
Widget _info() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
localizedDramaTitle(l10n, drama),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w800,
height: 1.15,
shadows: [Shadow(blurRadius: 12, color: Colors.black)],
),
), ),
const SizedBox(height: 6), );
Text( }
'${l10n.episodeLabel(episode.number)} · ${l10n.nowPlaying}',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w600,
shadows: [Shadow(blurRadius: 8, color: Colors.black)],
),
),
],
);
Widget _rail() => Column(
mainAxisSize: MainAxisSize.min,
children: [
_RailAction(
key: const Key('rail-like'),
icon: liked ? Icons.favorite : Icons.favorite_border,
label: l10n.like,
selected: liked,
onTap: onLike,
),
_RailAction(
key: const Key('rail-save'),
icon: saved ? Icons.bookmark : Icons.bookmark_border,
label: l10n.save,
selected: saved,
onTap: onSave,
),
_RailAction(
key: const Key('rail-download'),
icon: Icons.download_outlined,
label: l10n.downloads,
onTap: onDownload,
),
_RailAction(
icon: Icons.grid_view_outlined,
label: l10n.episodes,
onTap: onEpisodes,
),
_RailAction(
key: const Key('rail-comments'),
icon: Icons.chat_bubble_outline,
label: l10n.comments,
onTap: onComments,
),
_RailAction(
key: const Key('rail-share'),
icon: Icons.share_outlined,
label: l10n.share,
onTap: onShare,
),
if (onPip != null)
IconButton(
tooltip: l10n.pictureInPicture,
onPressed: onPip,
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(
width: _railTapSize,
height: _railTapSize,
),
color: AppColors.cream,
icon: const Icon(
Icons.picture_in_picture_alt_outlined,
size: _railIconSize,
),
),
],
);
Widget _fallbackNav() => AppBottomNav(
items: [
AppBottomNavItem(
tabKey: const Key('player-nav-home'),
icon: Icons.play_circle_outline_rounded,
selectedIcon: Icons.play_circle_fill_rounded,
label: l10n.homeTab,
selected: true,
onTap: () {},
),
AppBottomNavItem(
tabKey: const Key('player-nav-discover'),
icon: Icons.explore_outlined,
selectedIcon: Icons.explore_rounded,
label: l10n.discoverTab,
onTap: onDiscover,
),
AppBottomNavItem(
tabKey: const Key('player-nav-my'),
icon: Icons.video_library_outlined,
selectedIcon: Icons.video_library_rounded,
label: l10n.myTab,
onTap: onMy,
),
],
);
} }
const double _railIconSize = 28; class _TopAction extends StatelessWidget {
const double _railIconWeight = 300; const _TopAction({
const double _railTapSize = 44; super.key,
required this.tooltip,
required this.icon,
required this.onPressed,
});
class _RailAction extends StatelessWidget { final String tooltip;
const _RailAction({ final IconData icon;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 28,
height: 28,
child: IconButton(
tooltip: tooltip,
onPressed: onPressed,
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2C2D33),
foregroundColor: AppColors.onSurface,
),
icon: Icon(icon, size: 15),
),
);
}
}
class _PlayerAction extends StatelessWidget {
const _PlayerAction({
super.key, super.key,
required this.icon, required this.icon,
required this.label, required this.label,
required this.onTap, required this.onTap,
this.selected = false,
}); });
final IconData icon; final IconData icon;
final String label; final String label;
final VoidCallback? onTap; final VoidCallback onTap;
final bool selected;
@override @override
Widget build(BuildContext context) => IconButton( Widget build(BuildContext context) {
tooltip: label, return Material(
onPressed: onTap, color: const Color(0xFF2A2A30),
padding: EdgeInsets.zero, borderRadius: BorderRadius.circular(9),
constraints: const BoxConstraints.tightFor( clipBehavior: Clip.antiAlias,
width: _railTapSize, child: InkWell(
height: _railTapSize, onTap: onTap,
), child: SizedBox(
color: selected ? AppColors.blueSoft : AppColors.cream, height: 32,
icon: Icon(icon, size: _railIconSize, weight: _railIconWeight, fill: 0), child: Row(
); mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 14, color: AppColors.secondary),
const SizedBox(width: 3),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
);
}
} }
+160 -394
View File
@@ -3,27 +3,21 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import '../../../core/data/models/drama.dart'; import '../../../core/data/models/drama.dart';
import '../../../core/formatting/localized_content.dart'; import '../../../core/formatting/localized_content.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/glass_panel.dart'; import '../../../core/widgets/glass_panel.dart';
import '../../../core/widgets/remote_image.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'seek_progress_bar.dart';
/// Immersive home layer from `screens/01-home.html`. /// Home feed layer from `docs/移动端设计预览.html` screen 01.
///
/// Playback, ticket, progress, favorite, like, comment, and share callbacks are
/// all owned by [PlayerPage]; this widget only paints the design overlay.
class HomeFeedOverlay extends StatelessWidget { class HomeFeedOverlay extends StatelessWidget {
const HomeFeedOverlay({ const HomeFeedOverlay({
super.key, super.key,
required this.l10n, required this.l10n,
required this.drama, required this.drama,
required this.episode, required this.episode,
required this.controller,
required this.liked, required this.liked,
required this.saved, required this.saved,
required this.onLike, required this.onLike,
@@ -34,15 +28,12 @@ class HomeFeedOverlay extends StatelessWidget {
required this.onDownload, required this.onDownload,
required this.onClear, required this.onClear,
required this.onSearch, required this.onSearch,
required this.onScrubStart,
required this.onSeek,
this.onPip, this.onPip,
}); });
final AppLocalizations l10n; final AppLocalizations l10n;
final Drama drama; final Drama drama;
final Episode episode; final Episode episode;
final VideoPlayerController? controller;
final bool liked; final bool liked;
final bool saved; final bool saved;
final VoidCallback onLike; final VoidCallback onLike;
@@ -54,12 +45,9 @@ class HomeFeedOverlay extends StatelessWidget {
final VoidCallback onClear; final VoidCallback onClear;
final VoidCallback onSearch; final VoidCallback onSearch;
final VoidCallback? onPip; final VoidCallback? onPip;
final VoidCallback? onScrubStart;
final Future<void> Function(Duration)? onSeek;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final compact = MediaQuery.sizeOf(context).height < 620;
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Column( child: Column(
@@ -67,16 +55,16 @@ class HomeFeedOverlay extends StatelessWidget {
_header(context), _header(context),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.bottomRight, alignment: Alignment.centerRight,
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 12, 4), padding: const EdgeInsets.fromLTRB(0, 0, 10, 12),
child: _rail(compact), child: _rail(),
), ),
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 10), padding: const EdgeInsets.fromLTRB(14, 0, 14, 14),
child: _infoCard(context, compact), child: _infoCard(),
), ),
], ],
), ),
@@ -85,79 +73,35 @@ class HomeFeedOverlay extends StatelessWidget {
Widget _header(BuildContext context) { Widget _header(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 12, 4), padding: const EdgeInsets.fromLTRB(14, 6, 10, 4),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: Column( child: Text(
crossAxisAlignment: CrossAxisAlignment.start, localizedDramaTitle(l10n, drama),
children: [ key: const Key('home-feed-title'),
Row( maxLines: 1,
children: [ overflow: TextOverflow.ellipsis,
Container( style: const TextStyle(
width: 8, fontFamily: 'Outfit',
height: 8, color: AppColors.onSurface,
decoration: const BoxDecoration( fontSize: 13,
color: AppColors.blue, fontWeight: FontWeight.w800,
shape: BoxShape.circle, shadows: [Shadow(blurRadius: 8, color: Colors.black)],
), ),
),
const SizedBox(width: 7),
Flexible(
child: Text(
localizedDramaTitle(l10n, drama),
key: const Key('home-feed-title'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.onSurface,
fontSize: 17,
fontWeight: FontWeight.w700,
height: 1.1,
shadows: [
Shadow(blurRadius: 10, color: Colors.black87),
],
),
),
),
if (drama.isHot) ...[const SizedBox(width: 6), _hotBadge()],
],
),
const SizedBox(height: 3),
Text(
'${l10n.seasonOneVerticalCinema} · ${l10n.nowPlaying}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.64),
fontSize: 10.5,
fontWeight: FontWeight.w500,
shadows: const [
Shadow(blurRadius: 8, color: Colors.black87),
],
),
),
],
), ),
), ),
const SizedBox(width: 6), _RoundIcon(
_GlassIconButton( key: const Key('home-feed-search'),
tooltip: l10n.search, tooltip: l10n.search,
icon: Icons.search_rounded, icon: Icons.search_rounded,
onPressed: onSearch, onPressed: onSearch,
), ),
const SizedBox(width: 4), const SizedBox(width: 6),
_GlassIconButton( _RoundIcon(
tooltip: l10n.clearScreen, key: const Key('home-feed-more'),
icon: Icons.fit_screen_outlined,
onPressed: onClear,
),
const SizedBox(width: 4),
_GlassIconButton(
tooltip: l10n.more, tooltip: l10n.more,
icon: Icons.more_horiz_rounded, icon: Icons.menu_rounded,
onPressed: () => _openMore(context), onPressed: () => _openMore(context),
), ),
], ],
@@ -165,131 +109,53 @@ class HomeFeedOverlay extends StatelessWidget {
); );
} }
Widget _hotBadge() { Widget _rail() {
return DecoratedBox(
decoration: BoxDecoration(
color: AppColors.blue.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(5),
border: Border.all(color: AppColors.blue.withValues(alpha: 0.4)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
child: Text(
l10n.hot,
style: const TextStyle(
color: AppColors.blue,
fontSize: 8,
fontWeight: FontWeight.w800,
letterSpacing: 0.4,
height: 1.1,
),
),
),
);
}
Widget _rail(bool compact) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Semantics( _RailItem(
button: true,
label: saved ? l10n.followingTab : l10n.followSeries,
child: InkWell(
key: const Key('home-feed-follow'),
onTap: onSave,
customBorder: const CircleBorder(),
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
width: compact ? 40 : 44,
height: compact ? 40 : 44,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: AppColors.blue, width: 2),
color: AppColors.raised,
),
child: ClipOval(
child: RemoteImage(
drama.posterUrl,
fit: BoxFit.cover,
showFallbackIcon: true,
),
),
),
Positioned(
bottom: -4,
left: 14,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: saved ? AppColors.success : AppColors.blue,
shape: BoxShape.circle,
border: Border.all(color: AppColors.ink, width: 2),
),
child: Icon(
saved ? Icons.check_rounded : Icons.add_rounded,
color: AppColors.onPrimary,
size: 11,
),
),
),
],
),
),
),
const SizedBox(height: 8),
_RailAction(
compact: compact,
key: const Key('rail-like'), key: const Key('rail-like'),
icon: liked ? Icons.favorite_rounded : Icons.favorite_border_rounded, icon: liked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
label: l10n.like, label: l10n.like,
selected: liked, selected: liked,
onTap: onLike, onTap: onLike,
), ),
_RailAction( _RailItem(
compact: compact,
key: const Key('rail-save'),
icon: saved ? Icons.bookmark_rounded : Icons.bookmark_border_rounded,
label: l10n.save,
selected: saved,
onTap: onSave,
),
_RailAction(
compact: compact,
key: const Key('rail-comments'), key: const Key('rail-comments'),
icon: Icons.chat_bubble_outline_rounded, icon: Icons.chat_bubble_outline_rounded,
label: l10n.comments, label: l10n.comments,
onTap: onComments, onTap: onComments,
), ),
_RailAction( _RailItem(
compact: compact,
key: const Key('rail-share'), key: const Key('rail-share'),
icon: Icons.share_outlined, icon: Icons.ios_share_rounded,
label: l10n.share, label: l10n.share,
onTap: onShare, onTap: onShare,
), ),
_RailItem(
key: const Key('rail-save'),
icon: saved ? Icons.star_rounded : Icons.star_border_rounded,
label: l10n.favorite,
selected: saved,
onTap: onSave,
),
], ],
); );
} }
Widget _infoCard(BuildContext context, bool compact) { Widget _infoCard() {
final genres = [ final genre = drama.genre.isEmpty ? drama.category : drama.genre;
if (drama.genre.isNotEmpty) drama.genre,
if (drama.category.isNotEmpty) drama.category,
].take(2).toList();
return GlassPanel( return GlassPanel(
key: const Key('home-feed-info-card'), key: const Key('home-feed-info-card'),
radius: 26, radius: 14,
blur: 24, blur: 6,
padding: const EdgeInsets.fromLTRB(16, 13, 16, 12), padding: const EdgeInsets.fromLTRB(12, 11, 12, 11),
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0x260B0C10), Color(0x590B0C10)], colors: [Color(0xBF0F0F14), Color(0xA60F0F14)],
), ),
borderColor: Colors.white.withValues(alpha: 0.06),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -298,94 +164,73 @@ class HomeFeedOverlay extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: Wrap( child: Wrap(
spacing: 5, spacing: 6,
runSpacing: 4, runSpacing: 4,
children: [ children: [
for (var i = 0; i < genres.length; i++) if (genre.isNotEmpty)
_genreChip(genres[i], i.isEven), _tag(
genre,
background: AppColors.blue,
foreground: AppColors.onPrimary,
),
if (drama.category.isNotEmpty && drama.category != genre)
_tag(
drama.category,
background: const Color(0xFF333333),
foreground: const Color(0xFFDDDDDD),
),
], ],
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Container( Flexible(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), child: Text(
decoration: BoxDecoration( l10n.episodeOfTotal(episode.number, drama.episodeCount),
color: Colors.white.withValues(alpha: 0.06), key: const Key('home-feed-episode'),
borderRadius: BorderRadius.circular(999), maxLines: 1,
border: Border.all(color: AppColors.border), overflow: TextOverflow.ellipsis,
), textAlign: TextAlign.right,
child: Row( style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 9),
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 5,
height: 5,
child: DecoratedBox(
decoration: BoxDecoration(
color: AppColors.success,
shape: BoxShape.circle,
),
),
),
const SizedBox(width: 4),
Text(
'${l10n.episodeLabel(episode.number)} / '
'${drama.episodeCount}',
key: const Key('home-feed-episode'),
style: const TextStyle(
color: AppColors.cream,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
],
), ),
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 7),
Text( Text(
drama.synopsis, localizedEpisodeTitle(l10n, episode.number),
maxLines: compact ? 1 : 2, key: const Key('home-feed-episode-title'),
maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: AppColors.cream, color: AppColors.onSurface,
fontSize: 12, fontSize: 13,
height: 1.35, fontWeight: FontWeight.w800,
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 10),
SeekProgressBar(
valueListenable: controller,
semanticsLabel: l10n.seekVideo,
showTimes: false,
compact: true,
onScrubStart: onScrubStart,
onSeek: onSeek,
),
_timeRow(),
const SizedBox(height: 8),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: SizedBox( child: SizedBox(
height: 40, height: 34,
child: FilledButton.icon( child: FilledButton.icon(
key: const Key('home-feed-follow-series'), key: const Key('home-feed-follow-series'),
onPressed: onSave, onPressed: onSave,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: AppColors.blue, backgroundColor: AppColors.blue,
foregroundColor: AppColors.onPrimary, foregroundColor: AppColors.onPrimary,
shape: const StadiumBorder(), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppDimens.buttonRadius,
),
),
padding: EdgeInsets.zero,
textStyle: const TextStyle( textStyle: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
icon: Icon( icon: const Icon(Icons.play_arrow_rounded, size: 16),
saved ? Icons.check_rounded : Icons.add_rounded,
size: 16,
),
label: Text( label: Text(
saved ? l10n.followingTab : l10n.followSeries, saved ? l10n.followingTab : l10n.followSeries,
maxLines: 1, maxLines: 1,
@@ -395,11 +240,33 @@ class HomeFeedOverlay extends StatelessWidget {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_GlassAction( Expanded(
tooltip: l10n.episodes, child: SizedBox(
icon: Icons.grid_view_rounded, height: 34,
label: l10n.episodes, child: FilledButton(
onPressed: onEpisodes, key: const Key('home-feed-episodes'),
onPressed: onEpisodes,
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFF2A2A30),
foregroundColor: AppColors.onSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppDimens.buttonRadius,
),
),
padding: EdgeInsets.zero,
textStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
child: Text(
l10n.episodes,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
), ),
], ],
), ),
@@ -408,41 +275,29 @@ class HomeFeedOverlay extends StatelessWidget {
); );
} }
Widget _genreChip(String label, bool secondary) { Widget _tag(
final color = secondary ? AppColors.secondary : AppColors.blue; String label, {
required Color background,
required Color foreground,
}) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color.withValues(alpha: 0.15), color: background,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withValues(alpha: 0.3)),
), ),
child: Text( child: Text(
label, label,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: color, color: foreground,
fontSize: 10, fontSize: 8,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w800,
height: 1.1,
), ),
), ),
); );
} }
Widget _timeRow() {
final source = controller;
if (source == null) {
return const _Times(position: Duration.zero, duration: Duration.zero);
}
return ValueListenableBuilder<VideoPlayerValue>(
valueListenable: source,
builder: (context, value, _) =>
_Times(position: value.position, duration: value.duration),
);
}
Future<void> _openMore(BuildContext context) async { Future<void> _openMore(BuildContext context) async {
await showModalBottomSheet<void>( await showModalBottomSheet<void>(
context: context, context: context,
@@ -456,7 +311,7 @@ class HomeFeedOverlay extends StatelessWidget {
key: const Key('home-feed-download'), key: const Key('home-feed-download'),
leading: const Icon( leading: const Icon(
Icons.download_outlined, Icons.download_outlined,
color: AppColors.blueSoft, color: AppColors.secondary,
), ),
title: Text(l10n.downloads), title: Text(l10n.downloads),
onTap: () { onTap: () {
@@ -469,7 +324,7 @@ class HomeFeedOverlay extends StatelessWidget {
key: const Key('home-feed-pip'), key: const Key('home-feed-pip'),
leading: const Icon( leading: const Icon(
Icons.picture_in_picture_alt_outlined, Icons.picture_in_picture_alt_outlined,
color: AppColors.blueSoft, color: AppColors.secondary,
), ),
title: Text(l10n.pictureInPicture), title: Text(l10n.pictureInPicture),
onTap: () { onTap: () {
@@ -478,9 +333,10 @@ class HomeFeedOverlay extends StatelessWidget {
}, },
), ),
ListTile( ListTile(
key: const Key('home-feed-clear'),
leading: const Icon( leading: const Icon(
Icons.fit_screen_outlined, Icons.fit_screen_outlined,
color: AppColors.blueSoft, color: AppColors.secondary,
), ),
title: Text(l10n.clearScreen), title: Text(l10n.clearScreen),
onTap: () { onTap: () {
@@ -495,35 +351,9 @@ class HomeFeedOverlay extends StatelessWidget {
} }
} }
class _Times extends StatelessWidget { class _RoundIcon extends StatelessWidget {
const _Times({required this.position, required this.duration}); const _RoundIcon({
super.key,
final Duration position;
final Duration duration;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_format(position),
style: const TextStyle(color: AppColors.muted, fontSize: 9),
),
Text(
_format(duration),
style: const TextStyle(color: AppColors.muted, fontSize: 9),
),
],
),
);
}
}
class _GlassIconButton extends StatelessWidget {
const _GlassIconButton({
required this.tooltip, required this.tooltip,
required this.icon, required this.icon,
required this.onPressed, required this.onPressed,
@@ -531,140 +361,76 @@ class _GlassIconButton extends StatelessWidget {
final String tooltip; final String tooltip;
final IconData icon; final IconData icon;
final VoidCallback? onPressed; final VoidCallback onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: 34, width: 28,
height: 34, height: 28,
child: IconButton( child: IconButton(
tooltip: tooltip, tooltip: tooltip,
onPressed: onPressed, onPressed: onPressed,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.black.withValues(alpha: 0.28), backgroundColor: const Color(0xCC2C2D33),
foregroundColor: AppColors.onSurface, foregroundColor: AppColors.onSurface,
side: BorderSide(color: Colors.white.withValues(alpha: 0.16)),
), ),
icon: Icon(icon, size: 18), icon: Icon(icon, size: 14),
), ),
); );
} }
} }
class _GlassAction extends StatelessWidget { class _RailItem extends StatelessWidget {
const _GlassAction({ const _RailItem({
required this.tooltip,
required this.icon,
required this.label,
required this.onPressed,
});
final String tooltip;
final IconData icon;
final String label;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(999),
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: Colors.white.withValues(alpha: 0.15)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: AppColors.secondary, size: 15),
const SizedBox(width: 5),
Text(
label,
maxLines: 1,
style: const TextStyle(
color: AppColors.cream,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
);
}
}
class _RailAction extends StatelessWidget {
const _RailAction({
super.key, super.key,
required this.icon, required this.icon,
required this.label, required this.label,
required this.onTap, required this.onTap,
this.selected = false, this.selected = false,
this.compact = false,
}); });
final IconData icon; final IconData icon;
final String label; final String label;
final VoidCallback onTap; final VoidCallback onTap;
final bool selected; final bool selected;
final bool compact;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: compact ? 2 : 3), padding: const EdgeInsets.only(bottom: 14),
child: Column( child: Semantics(
mainAxisSize: MainAxisSize.min, button: true,
children: [ label: label,
GlassPanel( child: InkWell(
radius: 999, onTap: onTap,
blur: 16, borderRadius: BorderRadius.circular(22),
child: SizedBox( child: SizedBox(
width: compact ? 40 : 38, width: 44,
height: compact ? 40 : 38, child: Column(
child: IconButton( children: [
tooltip: label, Icon(
onPressed: onTap,
padding: EdgeInsets.zero,
icon: Icon(
icon, icon,
size: 19, size: 22,
color: selected ? AppColors.blue : AppColors.onSurface, color: selected ? AppColors.blue : AppColors.onSurface,
), ),
), const SizedBox(height: 2),
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.cream,
fontSize: 9,
shadows: [Shadow(blurRadius: 6, color: Colors.black)],
),
),
],
), ),
), ),
if (!compact) ...[ ),
const SizedBox(height: 2),
Text(
label,
maxLines: 1,
style: const TextStyle(
color: AppColors.cream,
fontSize: 9,
fontWeight: FontWeight.w600,
shadows: [Shadow(blurRadius: 6, color: Colors.black)],
),
),
],
],
), ),
); );
} }
} }
String _format(Duration value) {
final minutes = value.inMinutes.remainder(60).toString().padLeft(2, '0');
final seconds = value.inSeconds.remainder(60).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
@@ -0,0 +1,158 @@
// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/data/models/comment.dart';
import '../../../core/data/models/drama.dart';
import '../../../core/data/repositories/comment_repository.dart';
import '../../../core/data/repositories/interaction_repository.dart';
import '../../../core/data/services/comment_controller.dart';
import '../../../core/theme/app_colors.dart';
import '../../../l10n/app_localizations.dart';
class PlayerFeaturedComment extends StatefulWidget {
const PlayerFeaturedComment({
super.key,
required this.drama,
required this.onViewAll,
});
final Drama drama;
final VoidCallback onViewAll;
@override
State<PlayerFeaturedComment> createState() => _PlayerFeaturedCommentState();
}
class _PlayerFeaturedCommentState extends State<PlayerFeaturedComment> {
late final CommentController _controller;
@override
void initState() {
super.initState();
_controller = CommentController(
comments: context.read<CommentRepository>(),
interactions: context.read<InteractionRepository>(),
dramaId: widget.drama.id,
);
unawaited(_controller.load());
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: _controller,
builder: (context, _) {
if (_controller.comments.isEmpty) return const SizedBox.shrink();
return _card(context, _controller.comments.first);
},
);
}
Widget _card(BuildContext context, DramaComment comment) {
final l10n = AppLocalizations.of(context)!;
final time = _time(l10n, comment.createdAt);
return InkWell(
key: const Key('player-featured-comment'),
onTap: widget.onViewAll,
borderRadius: BorderRadius.circular(10),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(9),
decoration: BoxDecoration(
color: AppColors.card,
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 18,
height: 18,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.secondary.withValues(alpha: 0.2),
),
child: Text(
_initial(comment.nickname),
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 8,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 6),
Expanded(
child: Row(
children: [
Flexible(
child: Text(
comment.nickname.isEmpty
? l10n.localViewer
: comment.nickname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 10,
fontWeight: FontWeight.w700,
),
),
),
if (time != null) ...[
const SizedBox(width: 4),
Text(
'· $time',
style: const TextStyle(
color: Color(0xFF777777),
fontSize: 9,
),
),
],
],
),
),
],
),
const SizedBox(height: 5),
Text(
comment.content,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFFDDDDDD),
fontSize: 10,
height: 1.5,
),
),
],
),
),
);
}
String? _time(AppLocalizations l10n, DateTime? createdAt) {
if (createdAt == null) return null;
final diff = DateTime.now().difference(createdAt);
if (diff.inMinutes < 1) return null;
return l10n.minutesAgo(diff.inMinutes);
}
String _initial(String nickname) {
final trimmed = nickname.trim();
return trimmed.isEmpty ? '?' : trimmed.substring(0, 1).toUpperCase();
}
}
@@ -129,7 +129,7 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
activeTrackColor: AppColors.blue, activeTrackColor: AppColors.blue,
inactiveTrackColor: widget.compact inactiveTrackColor: widget.compact
? Colors.white.withValues(alpha: 0.18) ? Colors.white.withValues(alpha: 0.18)
: Colors.white30, : const Color(0xFF474248),
thumbColor: widget.compact ? AppColors.blue : Colors.white, thumbColor: widget.compact ? AppColors.blue : Colors.white,
thumbShape: RoundSliderThumbShape( thumbShape: RoundSliderThumbShape(
enabledThumbRadius: widget.compact enabledThumbRadius: widget.compact
+15 -17
View File
@@ -39,44 +39,42 @@ void main() {
expect(find.text('Continue Watching'), findsNothing); expect(find.text('Continue Watching'), findsNothing);
expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.byKey(const Key('home-all-dramas-title')), findsNothing);
expect(find.text('All'), findsOneWidget); expect(find.text('Recommended'), findsOneWidget);
expect(find.text('For You'), findsNothing); expect(find.text('For You'), findsNothing);
expect(
tester.getCenter(find.text('All')).dx,
lessThan(tester.getCenter(find.text('Hot')).dx),
);
final grid = tester.widget<SliverGrid>( final grid = tester.widget<SliverGrid>(
find.byKey(const Key('home-all-dramas-grid')), find.byKey(const Key('home-all-dramas-grid')),
); );
final delegate = final delegate =
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(delegate.crossAxisCount, 2); expect(delegate.crossAxisCount, 2);
expect(delegate.crossAxisSpacing, 12); expect(delegate.crossAxisSpacing, 10);
expect(delegate.mainAxisSpacing, 14); expect(delegate.mainAxisSpacing, 10);
expect(delegate.childAspectRatio, closeTo(0.55, 0.001)); expect(delegate.mainAxisExtent, 164);
expect( expect(
tester tester
.widget<SliverPadding>( .widget<SliverPadding>(
find.byKey(const Key('home-all-dramas-section')), find.byKey(const Key('home-all-dramas-section')),
) )
.padding, .padding,
const EdgeInsets.symmetric(horizontal: 16), const EdgeInsets.symmetric(horizontal: 14),
); );
expect(find.byType(GridView), findsNothing); expect(find.byType(GridView), findsNothing);
expect(find.byType(CustomScrollView), findsOneWidget); expect(find.byType(CustomScrollView), findsOneWidget);
expect(find.byType(RemoteImage), findsWidgets); expect(find.byType(RemoteImage), findsWidgets);
final cover = tester.widget<AspectRatio>( expect(
find.byKey(const Key('home-drama-cover-city-lights-demo')), tester
.getSize(find.byKey(const Key('home-drama-cover-city-lights-demo')))
.height,
120,
); );
expect(cover.aspectRatio, closeTo(3 / 4.2, 0.001));
expect( expect(
tester tester
.widget<Text>( .widget<Text>(
find.byKey(const Key('home-drama-title-city-lights-demo')), find.byKey(const Key('home-drama-title-city-lights-demo')),
) )
.maxLines, .maxLines,
2, 1,
); );
expect( expect(
tester tester
@@ -100,14 +98,14 @@ void main() {
await tester.tap(find.byKey(const Key('tab-discover'))); await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('The Signal'), findsOneWidget); expect(find.text('The Signal'), findsWidgets);
expect(find.text('City Lights & Velvet Nights'), findsOneWidget); expect(find.text('City Lights & Velvet Nights'), findsWidgets);
await tester.tap(find.text('Romance')); await tester.tap(find.text('Romance'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('The Signal'), findsNothing); expect(find.text('The Signal'), findsNothing);
expect(find.text('City Lights & Velvet Nights'), findsOneWidget); expect(find.text('City Lights & Velvet Nights'), findsWidgets);
expect( expect(
find.byKey(const Key('home-drama-card-the-signal-demo')), find.byKey(const Key('home-drama-card-the-signal-demo')),
findsNothing, findsNothing,
@@ -182,7 +180,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.byKey(const Key('home-all-dramas-title')), findsNothing);
expect(find.text('City Lights & Velvet Nights'), findsOneWidget); expect(find.text('City Lights & Velvet Nights'), findsWidgets);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
+13 -34
View File
@@ -21,7 +21,7 @@ import '../support/fake_haptics.dart';
import '../support/fake_interaction_repositories.dart'; import '../support/fake_interaction_repositories.dart';
void main() { void main() {
testWidgets('should_render_hero_cta_synopsis_and_two_column_episode_grid', ( testWidgets('should_render_preview_hero_cta_synopsis_and_episode_chips', (
tester, tester,
) async { ) async {
tester.view.physicalSize = const Size(390, 844); tester.view.physicalSize = const Size(390, 844);
@@ -32,45 +32,24 @@ void main() {
await tester.pumpWidget(_host(_detailDrama())); await tester.pumpWidget(_host(_detailDrama()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final hero = tester.widget<AspectRatio>( expect(tester.getSize(find.byKey(const Key('detail-hero'))).height, 190);
find.byKey(const Key('detail-hero')),
);
expect(hero.aspectRatio, closeTo(16 / 9, 0.001));
expect(find.byKey(const Key('detail-cta')), findsOneWidget); expect(find.byKey(const Key('detail-cta')), findsOneWidget);
expect(find.byKey(const Key('detail-favorite')), findsOneWidget); expect(tester.getSize(find.byKey(const Key('detail-cta'))).height, 40);
expect(find.byKey(const Key('detail-download')), findsOneWidget);
expect(find.byKey(const Key('detail-episodes-range')), findsOneWidget);
expect(find.byKey(const Key('detail-episode-chip-1')), findsOneWidget); expect(find.byKey(const Key('detail-episode-chip-1')), findsOneWidget);
expect( expect(find.byKey(const Key('detail-episode-chip-8')), findsOneWidget);
tester.getSize(find.byKey(const Key('detail-episode-chip-1'))),
const Size(46, 44),
);
final synopsis = tester.widget<Text>( final synopsis = tester.widget<Text>(
find.byKey(const Key('detail-synopsis')), find.byKey(const Key('detail-synopsis')),
); );
expect(synopsis.maxLines, 2); expect(synopsis.style?.fontSize, 11);
await tester.tap(find.byKey(const Key('detail-synopsis-toggle'))); expect(synopsis.style?.height, 1.6);
await tester.pumpAndSettle();
expect(
tester.widget<Text>(find.byKey(const Key('detail-synopsis'))).maxLines,
100,
);
final grid = tester.widget<SliverGrid>( await tester.tap(find.byKey(const Key('detail-more')));
find.descendant( await tester.pumpAndSettle();
of: find.byKey(const Key('detail-episode-grid')), expect(find.byKey(const Key('detail-favorite')), findsOneWidget);
matching: find.byType(SliverGrid), expect(find.byKey(const Key('detail-download')), findsOneWidget);
), expect(find.byKey(const Key('detail-share')), findsOneWidget);
); expect(find.byKey(const Key('detail-report')), findsOneWidget);
final delegate =
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(delegate.crossAxisCount, 2);
expect(find.byKey(const Key('detail-episode-card-1')), findsOneWidget);
expect(
tester.getSize(find.byKey(const Key('detail-episode-thumb-1'))),
const Size(96, 64),
);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
@@ -90,7 +69,7 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('detail-episode-card-3')), findsOneWidget); expect(find.byKey(const Key('detail-episode-chip-8')), findsOneWidget);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
} }
+64
View File
@@ -0,0 +1,64 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
import 'package:fruitshorts/app.dart';
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
import '../support/fake_catalog.dart';
import '../support/fake_category.dart';
import '../support/fake_haptics.dart';
import '../support/fake_interaction_repositories.dart';
import '../support/fake_notice_repository.dart';
import '../support/widget_player.dart';
void main() {
testWidgets('should_render_preview_home_card_without_hot_or_follow_avatar', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final catalog = FakeCatalogRepository();
await tester.pumpWidget(
LumaDramaApp(
playerFactory: createWidgetPlayer,
catalogRepository: catalog,
categoryRepository: FakeCategoryRepository(catalog: catalog),
watchHistoryRepository: InMemoryWatchHistoryRepository(),
favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas),
playbackRepository: FakePlaybackRepository(),
commentRepository: FakeCommentRepository(),
interactionRepository: FakeInteractionRepository(),
noticeRepository: FakeNoticeRepository(),
haptics: FakeHaptics(),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('home-feed-title')), findsOneWidget);
expect(find.byKey(const Key('home-feed-info-card')), findsOneWidget);
expect(find.byKey(const Key('home-feed-episode')), findsOneWidget);
expect(find.byKey(const Key('home-feed-follow-series')), findsOneWidget);
expect(find.byKey(const Key('home-feed-episodes')), findsOneWidget);
expect(
tester.getSize(find.byKey(const Key('home-feed-follow-series'))).height,
34,
);
expect(find.text('HOT'), findsNothing);
expect(find.textContaining('Season'), findsNothing);
expect(find.byKey(const Key('home-feed-follow')), findsNothing);
expect(tester.takeException(), isNull);
});
}
+18 -9
View File
@@ -29,11 +29,15 @@ void main() {
await _pumpApp(tester); await _pumpApp(tester);
await tester.tap(find.byKey(const Key('tab-discover'))); await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('City Lights & Velvet Nights')); await tester.tap(find.byKey(const Key('home-drama-card-city-lights-demo')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
_expectFullscreenOverlay(); _expectFullscreenOverlay();
expect(find.byType(PageView), findsOneWidget); expect(find.byType(PageView), findsOneWidget);
await tester.tap(find.byKey(const Key('player-more')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('player-more-download')), findsOneWidget);
expect(find.byKey(const Key('player-more-pip')), findsOneWidget);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
@@ -57,24 +61,29 @@ void main() {
await tester.tap(find.byKey(const Key('detail-episode-chip-2'))); await tester.tap(find.byKey(const Key('detail-episode-chip-2')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
_expectFullscreenOverlay(); _expectFullscreenOverlay();
expect(find.text('Episode 2 · Playing now'), findsOneWidget); expect(
tester.widget<Text>(find.byKey(const Key('player-title'))).data,
contains('Episode 2'),
);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
} }
void _expectFullscreenOverlay() { void _expectFullscreenOverlay() {
expect(find.byKey(const Key('rail-like')), findsOneWidget); expect(find.byKey(const Key('player-back')), findsOneWidget);
expect(find.byKey(const Key('rail-save')), findsOneWidget); expect(find.byKey(const Key('player-title')), findsOneWidget);
expect(find.byKey(const Key('rail-comments')), findsOneWidget); expect(find.byKey(const Key('player-more')), findsOneWidget);
expect(find.byKey(const Key('rail-share')), findsOneWidget); expect(find.byKey(const Key('player-play-toggle')), findsOneWidget);
expect(find.byTooltip('Picture in picture'), findsOneWidget); expect(find.byKey(const Key('player-episodes')), findsOneWidget);
expect(find.byTooltip('Clear screen'), findsOneWidget); expect(find.byKey(const Key('player-comments')), findsOneWidget);
expect(find.byKey(const Key('player-next')), findsOneWidget);
expect(find.byType(Slider), findsOneWidget); expect(find.byType(Slider), findsOneWidget);
expect(find.byKey(const Key('rail-like')), findsNothing);
expect(find.byKey(const Key('rail-save')), findsNothing);
expect(find.byKey(const Key('card-player-video-frame')), findsNothing); expect(find.byKey(const Key('card-player-video-frame')), findsNothing);
expect(find.byKey(const Key('card-player-progress')), findsNothing); expect(find.byKey(const Key('card-player-progress')), findsNothing);
expect(find.byKey(const Key('card-player-next')), findsNothing); expect(find.byKey(const Key('card-player-next')), findsNothing);
expect(find.byKey(const Key('player-title')), findsNothing);
} }
Future<void> _pumpApp(WidgetTester tester) async { Future<void> _pumpApp(WidgetTester tester) async {
+35 -27
View File
@@ -110,17 +110,19 @@ void main() {
await tester.tap(find.byKey(const Key('tab-home'))); await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('The Signal'), findsOneWidget); expect(find.text('The Signal'), findsOneWidget);
expect(find.byType(Slider), findsOneWidget); expect(find.byType(Slider), findsNothing);
expect(find.text('Home'), findsWidgets); expect(find.text('Home'), findsWidgets);
expect(find.textContaining('Playing now'), findsOneWidget); expect(find.byKey(const Key('home-feed-title')), findsOneWidget);
expect(find.text('Recommended'), findsNothing); expect(find.byKey(const Key('home-feed-info-card')), findsOneWidget);
expect(find.text('Following'), findsNothing); expect(find.byKey(const Key('rail-like')), findsOneWidget);
expect(find.byTooltip('Episodes'), findsWidgets); expect(find.byKey(const Key('rail-comments')), findsOneWidget);
expect(find.byTooltip('Clear screen'), findsOneWidget); expect(find.byKey(const Key('rail-share')), findsOneWidget);
expect(find.byTooltip('Picture in picture'), findsNothing); expect(find.byKey(const Key('rail-save')), findsOneWidget);
await tester.tap(find.byTooltip('More')); await tester.tap(find.byKey(const Key('home-feed-more')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Picture in picture'), findsOneWidget); expect(find.byKey(const Key('home-feed-download')), findsOneWidget);
expect(find.byKey(const Key('home-feed-pip')), findsOneWidget);
expect(find.byKey(const Key('home-feed-clear')), findsOneWidget);
await tester.binding.handlePopRoute(); await tester.binding.handlePopRoute();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Watch Full'), findsNothing); expect(find.text('Watch Full'), findsNothing);
@@ -139,14 +141,16 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-home'))); await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Clear screen')); await tester.tap(find.byKey(const Key('home-feed-more')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('home-feed-clear')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('home-tabs-bar')), findsNothing); expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
expect(find.byTooltip('Exit clear screen'), findsOneWidget); expect(find.byTooltip('Exit clear screen'), findsOneWidget);
await tester.tap(find.byTooltip('Exit clear screen')); await tester.tap(find.byTooltip('Exit clear screen'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget); expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
expect(find.byTooltip('Clear screen'), findsOneWidget); expect(find.byKey(const Key('home-feed-more')), findsOneWidget);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
@@ -233,12 +237,11 @@ void main() {
expect(find.text('我的'), findsWidgets); expect(find.text('我的'), findsWidgets);
await tester.drag(find.byType(ListView), const Offset(0, 700)); await tester.drag(find.byType(ListView), const Offset(0, 700));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('我的账户'), findsOneWidget); expect(find.byKey(const Key('mine-wallet')), findsOneWidget);
expect(find.text('首页'), findsOneWidget); expect(find.text('首页'), findsOneWidget);
await tester.tap(find.text('找剧')); await tester.tap(find.text('发现'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('全部'), findsOneWidget); expect(find.text('推荐'), findsOneWidget);
expect(find.text('热门'), findsOneWidget);
}); });
testWidgets( testWidgets(
@@ -259,7 +262,9 @@ void main() {
expect(find.text('City Lights & Velvet Nights'), findsOneWidget); expect(find.text('City Lights & Velvet Nights'), findsOneWidget);
await tester.tap(find.byKey(const Key('tab-discover'))); await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('City Lights & Velvet Nights')); await tester.tap(
find.byKey(const Key('home-drama-card-city-lights-demo')),
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(Slider), findsOneWidget); expect(find.byType(Slider), findsOneWidget);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
@@ -324,7 +329,7 @@ void main() {
await tester.tap(find.byKey(const Key('tab-discover'))); await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('All'), findsOneWidget); expect(find.text('Recommended'), findsOneWidget);
expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget);
expect(find.byKey(const Key('discover-profile')), findsOneWidget); expect(find.byKey(const Key('discover-profile')), findsOneWidget);
expect( expect(
@@ -341,7 +346,7 @@ void main() {
), ),
findsOneWidget, findsOneWidget,
); );
expect(find.byTooltip('Search'), findsNothing); expect(find.byTooltip('Search'), findsOneWidget);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
await tester.tap(find.byKey(const Key('tab-my'))); await tester.tap(find.byKey(const Key('tab-my')));
@@ -372,7 +377,7 @@ void main() {
await tester.tap(find.byKey(const Key('tab-discover'))); await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget);
expect(find.byTooltip('Search'), findsNothing); expect(find.byTooltip('Search'), findsOneWidget);
expect( expect(
Navigator.of( Navigator.of(
tester.element(find.byKey(const Key('home-all-dramas-grid'))), tester.element(find.byKey(const Key('home-all-dramas-grid'))),
@@ -396,7 +401,7 @@ void main() {
await tester.tap(find.byKey(const Key('tab-discover'))); await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget);
expect(find.byTooltip('Search'), findsNothing); expect(find.byTooltip('Search'), findsOneWidget);
expect( expect(
Navigator.of( Navigator.of(
tester.element(find.byKey(const Key('home-all-dramas-grid'))), tester.element(find.byKey(const Key('home-all-dramas-grid'))),
@@ -440,12 +445,17 @@ void main() {
await tester.tap(find.byKey(const Key('tab-discover'))); await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('City Lights & Velvet Nights')); await tester.tap(
find.byKey(const Key('home-drama-card-city-lights-demo')),
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(Slider), findsOneWidget); expect(find.byType(Slider), findsOneWidget);
await tester.drag(find.byType(PageView), const Offset(0, -550)); await tester.drag(find.byType(PageView), const Offset(0, -550));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Episode 2 · Playing now'), findsOneWidget); expect(
tester.widget<Text>(find.byKey(const Key('player-title'))).data,
contains('Episode 2'),
);
expect(find.textContaining('City Lights & Velvet Nights'), findsWidgets); expect(find.textContaining('City Lights & Velvet Nights'), findsWidgets);
}, },
); );
@@ -507,9 +517,9 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final keys = [ final keys = [
const Key('rail-like'), const Key('rail-like'),
const Key('rail-save'),
const Key('rail-comments'), const Key('rail-comments'),
const Key('rail-share'), const Key('rail-share'),
const Key('rail-save'),
]; ];
final tops = keys final tops = keys
.map((key) => tester.getTopLeft(find.byKey(key)).dy) .map((key) => tester.getTopLeft(find.byKey(key)).dy)
@@ -644,10 +654,8 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-my'))); await tester.tap(find.byKey(const Key('tab-my')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.scrollUntilVisible( await tester.drag(find.byType(ListView), const Offset(0, -320));
find.byKey(const Key('mine-library-tab-1')), await tester.pumpAndSettle();
300,
);
await tester.tap(find.byKey(const Key('mine-library-tab-1'))); await tester.tap(find.byKey(const Key('mine-library-tab-1')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('The Signal'), findsOneWidget); expect(find.text('The Signal'), findsOneWidget);