merge: 合并 K9 首页全部剧目网格
This commit is contained in:
commit
2c381be5e8
11 files changed
+498
-230
No files matched your search
@@ -55,6 +55,7 @@
|
||||
"animatedTab": "Animated",
|
||||
"recommendedTab": "Recommended",
|
||||
"noCategoryDramas": "No dramas in this category yet.",
|
||||
"allDramas": "All Dramas",
|
||||
"trendingTab": "Trending",
|
||||
"liveActionTab": "Live Action",
|
||||
"search": "Search",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"animatedTab": "Animasi",
|
||||
"recommendedTab": "Rekomendasi",
|
||||
"noCategoryDramas": "Belum ada drama di kategori ini.",
|
||||
"allDramas": "Semua Drama",
|
||||
"trendingTab": "Sedang Tren",
|
||||
"liveActionTab": "Live Action",
|
||||
"search": "Cari",
|
||||
|
||||
@@ -388,6 +388,12 @@ abstract class AppLocalizations {
|
||||
/// **'No dramas in this category yet.'**
|
||||
String get noCategoryDramas;
|
||||
|
||||
/// No description provided for @allDramas.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All Dramas'**
|
||||
String get allDramas;
|
||||
|
||||
/// No description provided for @trendingTab.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -157,6 +157,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get noCategoryDramas => 'No dramas in this category yet.';
|
||||
|
||||
@override
|
||||
String get allDramas => 'All Dramas';
|
||||
|
||||
@override
|
||||
String get trendingTab => 'Trending';
|
||||
|
||||
|
||||
@@ -157,6 +157,9 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get noCategoryDramas => 'Belum ada drama di kategori ini.';
|
||||
|
||||
@override
|
||||
String get allDramas => 'Semua Drama';
|
||||
|
||||
@override
|
||||
String get trendingTab => 'Sedang Tren';
|
||||
|
||||
|
||||
@@ -154,6 +154,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get noCategoryDramas => '这个分类暂时没有剧集。';
|
||||
|
||||
@override
|
||||
String get allDramas => '全部剧目';
|
||||
|
||||
@override
|
||||
String get trendingTab => '热门';
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"animatedTab": "漫剧",
|
||||
"recommendedTab": "推荐",
|
||||
"noCategoryDramas": "这个分类暂时没有剧集。",
|
||||
"allDramas": "全部剧目",
|
||||
"trendingTab": "热门",
|
||||
"liveActionTab": "真人剧",
|
||||
"search": "搜索",
|
||||
|
||||
@@ -17,6 +17,8 @@ import 'discover_binding.dart';
|
||||
import 'discover_logic.dart';
|
||||
import 'widgets/cover_carousel.dart';
|
||||
|
||||
part 'widgets/home_drama_grid.dart';
|
||||
|
||||
class DiscoverPage extends StatefulWidget {
|
||||
const DiscoverPage({
|
||||
super.key,
|
||||
@@ -101,40 +103,12 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
final categories = context.watch<CategoryController>();
|
||||
final homeBanners = context.watch<CatalogController>().feed.banners;
|
||||
final allDramas = [...?widget.dramas, ...categories.visibleDramas];
|
||||
final cards = _cardsFor(categories.visibleDramas, '');
|
||||
final content = SafeArea(
|
||||
bottom: !widget.embedded,
|
||||
child: Column(
|
||||
children: [
|
||||
_header(l10n),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: logic.scroll,
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: _carousel(l10n, homeBanners, allDramas),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 18, 16, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_categories(l10n, categories),
|
||||
const SizedBox(height: 18),
|
||||
_continueHeader(l10n),
|
||||
const SizedBox(height: 12),
|
||||
_dramaContent(l10n, categories, cards),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: _feed(l10n, categories, homeBanners, allDramas)),
|
||||
if (!widget.embedded) _bottomNav(l10n),
|
||||
],
|
||||
),
|
||||
@@ -150,6 +124,60 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _feed(
|
||||
AppLocalizations l10n,
|
||||
CategoryController categories,
|
||||
List<FeedBanner> banners,
|
||||
List<Drama> allDramas,
|
||||
) => CustomScrollView(
|
||||
controller: logic.scroll,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: _carousel(l10n, banners, allDramas),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 18, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_categories(l10n, categories),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
l10n.allDramas,
|
||||
key: const Key('home-all-dramas-title'),
|
||||
style: const TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_HomeDramaGrid(
|
||||
l10n: l10n,
|
||||
categories: categories,
|
||||
onOpen: (drama) => logic.openCard(
|
||||
context,
|
||||
drama,
|
||||
embedded: widget.embedded,
|
||||
onDramaSelected: widget.onDramaSelected,
|
||||
onAction: widget.onAction,
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 24)),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _header(AppLocalizations l10n) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 12, 8),
|
||||
@@ -239,164 +267,6 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _continueHeader(AppLocalizations l10n) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.continueWatching,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.2,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.seeAll,
|
||||
style: const TextStyle(
|
||||
color: AppColors.blueSoft,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _dramaContent(
|
||||
AppLocalizations l10n,
|
||||
CategoryController categories,
|
||||
List<_ShowcaseCard> cards,
|
||||
) {
|
||||
if (categories.loading && categories.feed.sections.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
);
|
||||
}
|
||||
if (categories.error != null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.categoriesError,
|
||||
style: const TextStyle(color: Color(0xFFE6E1E3)),
|
||||
),
|
||||
),
|
||||
TextButton(onPressed: categories.load, child: Text(l10n.retry)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (cards.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Text(
|
||||
categories.selectedSectionMissing
|
||||
? l10n.noCategoryDramas
|
||||
: l10n.noSearchResults,
|
||||
style: const TextStyle(color: Color(0xFFE6E1E3)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: 196,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
for (final card in cards) ...[
|
||||
_continueTile(l10n, card, categories.visibleDramas),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _continueTile(
|
||||
AppLocalizations l10n,
|
||||
_ShowcaseCard card,
|
||||
List<Drama> source,
|
||||
) {
|
||||
final drama = logic.dramaFor(card.dramaId, source);
|
||||
return SizedBox(
|
||||
width: 132,
|
||||
child: InkWell(
|
||||
onTap: () => logic.openCard(
|
||||
context,
|
||||
drama,
|
||||
embedded: widget.embedded,
|
||||
onDramaSelected: widget.onDramaSelected,
|
||||
onAction: widget.onAction,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
RemoteImage(card.image),
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0x00000000), Color(0xE6000000)],
|
||||
stops: [0.45, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (drama == null)
|
||||
const ColoredBox(
|
||||
color: Color(0x99000000),
|
||||
child: Center(
|
||||
child: Icon(Icons.image_outlined, color: Colors.white),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
card.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
drama == null ? l10n.videoPending : card.genre,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0xE6FFFFFF),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _categories(AppLocalizations l10n, CategoryController categories) {
|
||||
final choices = <({String code, String label})>[
|
||||
(code: CategoryController.allCode, label: l10n.forYou),
|
||||
@@ -523,38 +393,6 @@ class _DiscoverNavItem extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
class _ShowcaseCard {
|
||||
const _ShowcaseCard({
|
||||
required this.title,
|
||||
required this.genre,
|
||||
required this.image,
|
||||
this.dramaId,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String genre;
|
||||
final String image;
|
||||
final String? dramaId;
|
||||
}
|
||||
|
||||
/// 分区由服务端下发:0 为全部,其余按分区顺序对应筛选菜单(热门 / 最新 / 各分类)。
|
||||
List<_ShowcaseCard> _cardsFor(List<Drama> dramas, String query) {
|
||||
final normalized = query.trim().toLowerCase();
|
||||
return [
|
||||
for (final drama in dramas)
|
||||
if (normalized.isEmpty ||
|
||||
drama.title.toLowerCase().contains(normalized) ||
|
||||
drama.genre.toLowerCase().contains(normalized) ||
|
||||
drama.category.toLowerCase().contains(normalized))
|
||||
_ShowcaseCard(
|
||||
title: drama.title,
|
||||
genre: drama.genre.isEmpty ? drama.category : drama.genre,
|
||||
image: drama.posterUrl,
|
||||
dramaId: drama.id,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
const _accent = AppColors.blueSoft;
|
||||
const _coin = Color(0xFFF5C451);
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
part of '../discover_view.dart';
|
||||
|
||||
class _HomeDramaGrid extends StatelessWidget {
|
||||
const _HomeDramaGrid({
|
||||
required this.l10n,
|
||||
required this.categories,
|
||||
required this.onOpen,
|
||||
});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final CategoryController categories;
|
||||
final ValueChanged<Drama> onOpen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (categories.loading && categories.feed.sections.isEmpty) {
|
||||
return const SliverPadding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (categories.error != null) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 18),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.categoriesError,
|
||||
style: const TextStyle(color: AppColors.muted),
|
||||
),
|
||||
),
|
||||
TextButton(onPressed: categories.load, child: Text(l10n.retry)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final dramas = categories.visibleDramas;
|
||||
if (dramas.isEmpty) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 24),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
categories.selectedSectionMissing
|
||||
? l10n.noCategoryDramas
|
||||
: l10n.noSearchResults,
|
||||
style: const TextStyle(color: AppColors.muted),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
key: const Key('home-all-dramas-section'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverGrid(
|
||||
key: const Key('home-all-dramas-grid'),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 2 / 3,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => _HomeDramaCard(
|
||||
l10n: l10n,
|
||||
drama: dramas[index],
|
||||
onTap: () => onOpen(dramas[index]),
|
||||
),
|
||||
childCount: dramas.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeDramaCard extends StatelessWidget {
|
||||
const _HomeDramaCard({
|
||||
required this.l10n,
|
||||
required this.drama,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final Drama drama;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final genre = drama.genre.isEmpty ? drama.category : drama.genre;
|
||||
return InkWell(
|
||||
key: Key('home-drama-card-${drama.id}'),
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: AspectRatio(
|
||||
key: Key('home-drama-cover-${drama.id}'),
|
||||
aspectRatio: 2 / 3,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
RemoteImage(
|
||||
drama.posterUrl,
|
||||
fit: BoxFit.cover,
|
||||
showFallbackIcon: true,
|
||||
),
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0x00000000), Color(0xE6000000)],
|
||||
stops: [0.4, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
drama.title,
|
||||
key: Key('home-drama-title-${drama.id}'),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
genre.isEmpty ? l10n.videoPending : genre,
|
||||
key: Key('home-drama-genre-${drama.id}'),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0xE6FFFFFF),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
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/models/content_category.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/data/models/home_feed.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/category_repository.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 'package:fruitshorts/core/widgets/remote_image.dart';
|
||||
|
||||
import '../support/fake_catalog.dart';
|
||||
import '../support/fake_interaction_repositories.dart';
|
||||
import '../support/fake_notice_repository.dart';
|
||||
import '../support/widget_player.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'should_show_all_dramas_in_two_column_sliver_grid_without_continue_watching',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
await _pumpApp(tester, categoryRepository: _GridCategoryRepository());
|
||||
await tester.tap(find.byKey(const Key('tab-discover')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Continue Watching'), findsNothing);
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
|
||||
final grid = tester.widget<SliverGrid>(
|
||||
find.byKey(const Key('home-all-dramas-grid')),
|
||||
);
|
||||
final delegate =
|
||||
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
|
||||
expect(delegate.crossAxisCount, 2);
|
||||
expect(delegate.crossAxisSpacing, 12);
|
||||
expect(delegate.mainAxisSpacing, 16);
|
||||
expect(delegate.childAspectRatio, closeTo(2 / 3, 0.001));
|
||||
expect(
|
||||
tester
|
||||
.widget<SliverPadding>(
|
||||
find.byKey(const Key('home-all-dramas-section')),
|
||||
)
|
||||
.padding,
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
);
|
||||
expect(find.byType(GridView), findsNothing);
|
||||
expect(find.byType(CustomScrollView), findsOneWidget);
|
||||
expect(find.byType(RemoteImage), findsWidgets);
|
||||
|
||||
final cover = tester.widget<AspectRatio>(
|
||||
find.byKey(const Key('home-drama-cover-city-lights-demo')),
|
||||
);
|
||||
expect(cover.aspectRatio, closeTo(2 / 3, 0.001));
|
||||
expect(
|
||||
tester
|
||||
.widget<Text>(
|
||||
find.byKey(const Key('home-drama-title-city-lights-demo')),
|
||||
)
|
||||
.maxLines,
|
||||
2,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<Text>(
|
||||
find.byKey(const Key('home-drama-genre-city-lights-demo')),
|
||||
)
|
||||
.maxLines,
|
||||
1,
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('should_update_grid_when_category_is_selected', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
await _pumpApp(tester, categoryRepository: _GridCategoryRepository());
|
||||
await tester.tap(find.byKey(const Key('tab-discover')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('The Signal'), findsOneWidget);
|
||||
expect(find.text('City Lights & Velvet Nights'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Romance'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('The Signal'), findsNothing);
|
||||
expect(find.text('City Lights & Velvet Nights'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('home-drama-card-the-signal-demo')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('home-drama-card-city-lights-demo')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should_keep_empty_messages_when_feed_has_no_dramas', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
await _pumpApp(
|
||||
tester,
|
||||
categoryRepository: _GridCategoryRepository(
|
||||
discoverFeed: const HomeFeed(),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('tab-discover')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
expect(find.text('No matching dramas.'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Romance'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('No dramas in this category yet.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_show_retry_when_category_feed_fails', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
await _pumpApp(tester, categoryRepository: _FailingCategoryRepository());
|
||||
await tester.tap(find.byKey(const Key('tab-discover')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
expect(find.text('Categories could not be loaded.'), findsOneWidget);
|
||||
expect(find.text('Retry'), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-all-dramas-grid')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_fit_two_column_grid_on_compact_screen', (tester) async {
|
||||
tester.view.physicalSize = const Size(320, 568);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
await _pumpApp(tester, categoryRepository: _GridCategoryRepository());
|
||||
await tester.tap(find.byKey(const Key('tab-discover')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.byKey(const Key('home-drama-card-city-lights-demo')),
|
||||
180,
|
||||
scrollable: find.byType(Scrollable).last,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
expect(find.text('City Lights & Velvet Nights'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpApp(
|
||||
WidgetTester tester, {
|
||||
required CategoryRepository categoryRepository,
|
||||
}) async {
|
||||
final catalog = FakeCatalogRepository();
|
||||
await tester.pumpWidget(
|
||||
LumaDramaApp(
|
||||
playerFactory: createWidgetPlayer,
|
||||
catalogRepository: catalog,
|
||||
categoryRepository: categoryRepository,
|
||||
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
||||
favoriteRepository: InMemoryFavoriteRepository(),
|
||||
playbackRepository: FakePlaybackRepository(),
|
||||
commentRepository: FakeCommentRepository(),
|
||||
interactionRepository: FakeInteractionRepository(),
|
||||
noticeRepository: FakeNoticeRepository(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
class _GridCategoryRepository implements CategoryRepository {
|
||||
_GridCategoryRepository({HomeFeed? discoverFeed})
|
||||
: discoverFeed = discoverFeed ?? _defaultDiscoverFeed;
|
||||
|
||||
final HomeFeed discoverFeed;
|
||||
|
||||
@override
|
||||
Future<List<ContentCategory>> fetchCategories() async => const [
|
||||
ContentCategory(id: '7', name: 'Romance'),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() async => discoverFeed;
|
||||
}
|
||||
|
||||
class _FailingCategoryRepository implements CategoryRepository {
|
||||
@override
|
||||
Future<List<ContentCategory>> fetchCategories() async => const [
|
||||
ContentCategory(id: '7', name: 'Romance'),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() async {
|
||||
throw StateError('discover feed unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
final _defaultDiscoverFeed = HomeFeed(
|
||||
sections: [
|
||||
FeedSection(code: 'hot', title: 'Hot', dramas: demoDramas),
|
||||
FeedSection(
|
||||
code: 'category-7',
|
||||
title: 'Romance',
|
||||
dramas: [
|
||||
Drama(
|
||||
id: 'city-lights-demo',
|
||||
title: 'City Lights & Velvet Nights',
|
||||
synopsis: 'City Lights & Velvet Nights synopsis',
|
||||
posterUrl: '/files/city-lights-demo.png',
|
||||
genre: 'Urban Romance',
|
||||
detailLoaded: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
+11
-11
@@ -155,7 +155,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(appUnderTest());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Continue Watching'), findsOneWidget);
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
@@ -186,15 +186,15 @@ void main() {
|
||||
.active,
|
||||
isFalse,
|
||||
);
|
||||
expect(find.text('Continue Watching'), findsOneWidget);
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('drawer-discover')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Continue Watching'), findsOneWidget);
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
expect(
|
||||
Navigator.of(tester.element(find.text('Continue Watching'))).canPop(),
|
||||
Navigator.of(tester.element(find.text('All Dramas'))).canPop(),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
@@ -313,7 +313,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('For You'), findsOneWidget);
|
||||
expect(find.text('Continue Watching'), findsOneWidget);
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
expect(find.text('Rewards'), findsOneWidget);
|
||||
expect(find.text(r'$ 0'), findsOneWidget);
|
||||
expect(find.text('Urban Romance', skipOffstage: false), findsOneWidget);
|
||||
@@ -345,10 +345,10 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Home'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Continue Watching'), findsOneWidget);
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
expect(find.byTooltip('Search'), findsNothing);
|
||||
expect(
|
||||
Navigator.of(tester.element(find.text('Continue Watching'))).canPop(),
|
||||
Navigator.of(tester.element(find.text('All Dramas'))).canPop(),
|
||||
isFalse,
|
||||
);
|
||||
|
||||
@@ -365,10 +365,10 @@ void main() {
|
||||
|
||||
await tester.tap(find.byKey(const Key('tab-discover')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Continue Watching'), findsOneWidget);
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
expect(find.byTooltip('Search'), findsNothing);
|
||||
expect(
|
||||
Navigator.of(tester.element(find.text('Continue Watching'))).canPop(),
|
||||
Navigator.of(tester.element(find.text('All Dramas'))).canPop(),
|
||||
isFalse,
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('tab-my-list')));
|
||||
@@ -564,7 +564,7 @@ void main() {
|
||||
await tester.pumpWidget(appUnderTest());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Continue Watching'), findsOneWidget);
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Quick access'), findsOneWidget);
|
||||
@@ -572,7 +572,7 @@ void main() {
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Quick access'), findsNothing);
|
||||
expect(find.text('Continue Watching'), findsOneWidget);
|
||||
expect(find.text('All Dramas'), findsOneWidget);
|
||||
expect(
|
||||
Navigator.of(
|
||||
tester.element(find.byKey(const Key('home-tabs-bar'))),
|
||||
|
||||
Reference in new issue
Block a user