feat(ui): 校正首页发现详情播放器预览
This commit is contained in:
1 parent
87db548591
commit
21d457fd98
13 files changed
+1218
-1684
No files matched your search
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user