fix(home): 修正首页分类与卡片布局

This commit is contained in:
Zhou Weidong committed 2026-10-22 18:14:33 +08:00
1 parent 10477562d4
commit aa4870cce6
14 files changed
+573 -459

No files matched your search

+7
View File
@@ -1108,3 +1108,10 @@ $ flutter test
解读:首页搜索/页签/二级标签、三类卡片字体与标签、5:6 封面、网格间距、
筛选 chip、我的标题和追剧双列 320 宽度适配均有测试覆盖。目标组件均低于
500 行;`pubspec.lock` 未提交,未操作真机。
## 2026-10-22 首页布局对齐修正
- 一级分类改为页内切换 6 个列表,选中 18/w700 白字、未选中 15/w400 灰字,无下划线。
- 二级 chips 与一级行左对齐可滑动;未选项纯文字,选中项深灰胶囊。
- 首页、剧单、全部短剧、追剧卡片移除底板/描边,标题与题材从封面左边起始,题材改为 `|` 分隔纯文字。
- 全部短剧的地区、字幕、配音筛选 UI 已占位;以下接口待补:地区、字幕/配音筛选数据。
+8 -2
View File
@@ -98,9 +98,15 @@ abstract final class AppDimens {
static const double cardTitleTagGap = 6;
static const double cardCoverAspectRatio = 5 / 6;
static const double homeCardBodyExtent = 84;
static const double homePrimaryTabFont = 16;
static const double homePrimaryTabFont = 15;
static const double homePrimaryTabSelectedFont = 18;
static const double homePrimaryRowHeight = 30;
static const Duration homePrimaryTabAnimationDuration = Duration(
milliseconds: 180,
);
static const double homeSecondaryTagFont = 13;
static const double homeSectionIndicator = 2;
static const double homePrimaryCategoryGap = 24;
static const double homeSecondaryTagGap = 18;
static const double homeIconSlot = 44;
static const double homeHeaderIcon = 22;
+13 -33
View File
@@ -41,6 +41,7 @@ class FollowingGridCard extends StatelessWidget {
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
AspectRatio(
key: Key('following-cover-$dramaId'),
@@ -119,6 +120,7 @@ class FollowingGridCard extends StatelessWidget {
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.cardTitleFont,
@@ -161,39 +163,17 @@ class FollowingGridCard extends StatelessWidget {
}
Widget _tags() {
final visible = tags.take(2).toList(growable: false);
return Row(
children: [
for (final (index, tag) in visible.indexed)
Flexible(
child: Padding(
padding: EdgeInsets.only(
right: index == visible.length - 1 ? 0 : AppDimens.spaceXs,
),
child: Container(
key: Key('following-tag-$dramaId-$index'),
height: AppDimens.cardTagHeight,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.cardTagHorizontalPadding,
),
decoration: BoxDecoration(
color: AppColors.contentSurface,
borderRadius: BorderRadius.circular(AppDimens.cardTagRadius),
),
child: Text(
tag,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.cardTagFont,
),
),
),
),
),
],
final visible = tags.take(3).toList(growable: false);
return Text(
visible.join(' | '),
key: Key('following-tags-$dramaId'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.cardTagFont,
),
);
}
}
+2 -8
View File
@@ -26,8 +26,6 @@ import '../discover/discover_view.dart';
import '../following/following_page.dart';
import '../filter/filter_page.dart';
import '../mine/mine_types.dart';
import '../new_releases/new_releases_page.dart';
import '../ranking/ranking_page.dart';
import '../recommend/recommend_page.dart';
import '../mine/mine_view.dart';
import '../store/vip_page.dart';
@@ -284,9 +282,6 @@ class _HomeTabsState extends State<_HomeTabs> {
onSearch: () => _openSearch(context),
onOpenVip: () => unawaited(_openVip(context)),
onOpenWallet: () => unawaited(_openWallet(context)),
onOpenNewReleases: () => _pushPage(context, const NewReleasesPage()),
onOpenCategories: () => _pushPage(context, const FilterPage()),
onOpenRanking: () => _pushPage(context, const RankingPage()),
onOpenFilter: () => _pushPage(context, const FilterPage()),
),
onOpenDrama: (drama) => unawaited(logic.playDrama(context, drama)),
@@ -324,9 +319,8 @@ class _HomeTabsState extends State<_HomeTabs> {
Future<void> _openWallet(BuildContext context) async {
if (!await ensureSignedIn(context) || !context.mounted) return;
await Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => const WalletPage()));
await Navigator.of(context)
.push<void>(MaterialPageRoute(builder: (_) => const WalletPage()));
}
Widget _my(BuildContext context) {
+47 -72
View File
@@ -29,49 +29,46 @@ class HomeFeedCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
return Material(
key: Key('home-feed-card-${drama.id}'),
decoration: BoxDecoration(
color: AppColors.contentCard,
color: AppColors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
border: Border.all(color: AppColors.contentBorder),
),
child: Material(
color: AppColors.transparent,
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_cover(l10n),
Expanded(
child: Padding(
padding: const EdgeInsets.all(AppDimens.homeCardPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
drama.title,
key: Key('home-feed-title-${drama.id}'),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.navSelected,
fontSize: AppDimens.cardTitleFont,
fontWeight: FontWeight.w500,
height: AppDimens.cardTitleLineHeight,
),
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
_cover(l10n),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
top: AppDimens.cardCoverTitleGap,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
drama.title,
key: Key('home-feed-title-${drama.id}'),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
style: const TextStyle(
color: AppColors.navSelected,
fontSize: AppDimens.cardTitleFont,
fontWeight: FontWeight.w500,
height: AppDimens.cardTitleLineHeight,
),
const SizedBox(height: AppDimens.cardTitleTagGap),
_tags(),
],
),
),
const SizedBox(height: AppDimens.cardTitleTagGap),
_tags(),
],
),
),
],
),
),
],
),
),
);
@@ -180,39 +177,17 @@ class HomeFeedCard extends StatelessWidget {
Widget _tags() {
final tags = _cardTags();
if (tags.isEmpty) return const SizedBox.shrink();
return Row(
children: [
for (final (index, tag) in tags.indexed)
Flexible(
child: Padding(
padding: EdgeInsets.only(
right: index == tags.length - 1 ? 0 : AppDimens.spaceXs,
),
child: Container(
key: Key('home-feed-tag-${drama.id}-$index'),
height: AppDimens.cardTagHeight,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.cardTagHorizontalPadding,
),
decoration: BoxDecoration(
color: AppColors.contentSurface,
borderRadius: BorderRadius.circular(AppDimens.cardTagRadius),
),
child: Text(
tag,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.cardTagFont,
letterSpacing: 0,
),
),
),
),
),
],
return Text(
tags.join(' | '),
key: Key('home-feed-tags-${drama.id}'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.cardTagFont,
letterSpacing: 0,
),
);
}
@@ -222,7 +197,7 @@ class HomeFeedCard extends StatelessWidget {
.expand((value) => value.split(_tagSplitter))
.map((value) => value.trim())
.where((value) => value.isNotEmpty && seen.add(value))
.take(2)
.take(3)
.toList(growable: false);
}
}
+194 -159
View File
@@ -42,8 +42,27 @@ class HomeFeedGrid extends StatefulWidget {
}
class _HomeFeedGridState extends State<HomeFeedGrid> {
String? _selectedCategory;
String? _selectedTag;
HomePrimaryTab _selectedPrimary = HomePrimaryTab.featured;
late final Map<HomePrimaryTab, ScrollController> _scrollControllers;
final Map<HomePrimaryTab, String?> _selectedTags = {
for (final tab in HomePrimaryTab.values) tab: null,
};
@override
void initState() {
super.initState();
_scrollControllers = {
for (final tab in HomePrimaryTab.values) tab: ScrollController(),
};
}
@override
void dispose() {
for (final controller in _scrollControllers.values) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
@@ -53,14 +72,10 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
final wallet = context.watch<WalletController>();
final shortcuts = _categoryShortcuts(categories, l10n);
final tags = _availableTags(l10n);
final activeCategory =
shortcuts.any((shortcut) => shortcut.categoryName == _selectedCategory)
? _selectedCategory
final activeTag =
tags.any((tag) => tag.id == _selectedTags[_selectedPrimary])
? _selectedTags[_selectedPrimary]
: null;
final activeTag = tags.any((tag) => tag.id == _selectedTag)
? _selectedTag
: null;
final dramas = _filteredDramas(activeCategory, activeTag);
final walletBadge = auth.isSignedIn && wallet.snapshot != null
? wallet.balance
: null;
@@ -69,102 +84,122 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
color: AppColors.appBackground,
child: SafeArea(
bottom: false,
child: LayoutBuilder(
builder: (context, constraints) {
final itemWidth =
(constraints.maxWidth -
AppDimens.feedPageHorizontal * 2 -
AppDimens.homeGridGap * 2) /
3;
final itemExtent = itemWidth * 6 / 5 + AppDimens.homeCardBodyExtent;
return NotificationListener<ScrollNotification>(
onNotification: _onScroll,
child: AppPullToRefresh(
key: const Key('home-grid-refresh'),
onRefresh: _refresh,
child: CustomScrollView(
key: const Key('home-grid-scroll'),
physics: AppPullToRefresh.physics,
slivers: [
SliverToBoxAdapter(
child: HomeGridHeader(
callbacks: widget.callbacks,
categories: shortcuts,
tags: tags,
selectedCategory: activeCategory,
selectedTag: activeTag,
onCategorySelected: _selectCategory,
onTagSelected: (tag) =>
setState(() => _selectedTag = tag),
walletBadge: walletBadge,
),
),
if (widget.feedController.loadingFirstPage)
const SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
)
else if (dramas.isEmpty)
SliverFillRemaining(
hasScrollBody: false,
child: _emptyState(l10n, activeCategory, activeTag),
)
else
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
),
sliver: SliverGrid(
key: const Key('home-grid-cards'),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: AppDimens.homeGridGap,
mainAxisSpacing: AppDimens.homeGridRowGap,
mainAxisExtent: itemExtent,
),
delegate: SliverChildBuilderDelegate(
(context, index) => HomeFeedCard(
drama: dramas[index],
onTap: () => widget.onOpenDrama(dramas[index]),
),
childCount: dramas.length,
),
),
),
if (dramas.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
bottom: AppDimens.spaceXl,
),
child: PagedListFooter(
loading: widget.feedController.loadingMore,
hasMore: widget.feedController.hasMore,
hasItems: dramas.isNotEmpty,
errorMessage:
widget.feedController.lastError == null
? null
: l10n.catalogError,
onRetry: widget.feedController.loadMore,
),
),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HomeGridHeader(
callbacks: widget.callbacks,
categories: shortcuts,
tags: tags,
selectedPrimary: _selectedPrimary,
selectedTag: activeTag,
onPrimarySelected: (tab) =>
setState(() => _selectedPrimary = tab),
onTagSelected: (tag) =>
setState(() => _selectedTags[_selectedPrimary] = tag),
walletBadge: walletBadge,
),
Expanded(
child: IndexedStack(
index: _selectedPrimary.index,
children: [
for (final tab in HomePrimaryTab.values)
_tabGrid(context, l10n, tab: tab, shortcuts: shortcuts),
],
),
);
},
),
],
),
),
);
}
Widget _tabGrid(
BuildContext context,
AppLocalizations l10n, {
required HomePrimaryTab tab,
required List<HomeGridCategoryShortcut> shortcuts,
}) {
final tag = _selectedTags[tab];
final dramas = _dramasFor(tab, tag, shortcuts);
return LayoutBuilder(
builder: (context, constraints) {
final itemWidth =
(constraints.maxWidth -
AppDimens.feedPageHorizontal * 2 -
AppDimens.homeGridGap * 2) /
3;
final itemExtent = itemWidth * 6 / 5 + AppDimens.homeCardBodyExtent;
return NotificationListener<ScrollNotification>(
onNotification: _onScroll,
child: AppPullToRefresh(
key: const Key('home-grid-refresh'),
onRefresh: _refresh,
child: CustomScrollView(
key: const Key('home-grid-scroll'),
controller: _scrollControllers[tab],
physics: AppPullToRefresh.physics,
slivers: [
if (widget.feedController.loadingFirstPage)
const SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
)
else if (dramas.isEmpty)
SliverFillRemaining(
hasScrollBody: false,
child: _emptyState(l10n, tab, tag),
)
else
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
),
sliver: SliverGrid(
key: const Key('home-grid-cards'),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: AppDimens.homeGridGap,
mainAxisSpacing: AppDimens.homeGridRowGap,
mainAxisExtent: itemExtent,
),
delegate: SliverChildBuilderDelegate(
(context, index) => HomeFeedCard(
drama: dramas[index],
onTap: () => widget.onOpenDrama(dramas[index]),
),
childCount: dramas.length,
),
),
),
if (dramas.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
child: PagedListFooter(
loading: widget.feedController.loadingMore,
hasMore: widget.feedController.hasMore,
hasItems: dramas.isNotEmpty,
errorMessage: widget.feedController.lastError == null
? null
: l10n.catalogError,
onRetry: widget.feedController.loadMore,
),
),
),
],
),
),
);
},
);
}
Future<void> _refresh() async {
await widget.feedController.refreshCurrent();
}
@@ -182,37 +217,24 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
return false;
}
void _selectCategory(String? category) {
setState(() {
_selectedCategory = category;
_selectedTag = null;
});
}
List<HomeGridCategoryShortcut> _categoryShortcuts(
List<ContentCategory> categories,
AppLocalizations l10n,
) {
final shortcuts = <HomeGridCategoryShortcut>[];
final costume = _findCategory(categories, _costumeAliases);
final community = _findCategory(categories, _communityAliases);
if (costume != null) {
shortcuts.add(
HomeGridCategoryShortcut(
label: l10n.homeSectionCostume,
categoryName: costume.name,
),
);
}
if (community != null) {
shortcuts.add(
HomeGridCategoryShortcut(
label: l10n.homeSectionCommunity,
categoryName: community.name,
),
);
}
return shortcuts;
return [
HomeGridCategoryShortcut(
label: l10n.homeSectionCostume,
categoryName: costume?.name ?? '',
tab: HomePrimaryTab.costume,
),
HomeGridCategoryShortcut(
label: l10n.homeSectionCommunity,
categoryName: community?.name ?? '',
tab: HomePrimaryTab.community,
),
];
}
ContentCategory? _findCategory(
@@ -227,46 +249,59 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
}
List<HomeGridTagOption> _availableTags(AppLocalizations l10n) {
final definitions = [
// UI 先完整展示筛选维度;后续接口补齐题材字段时只替换数据来源。
return [
HomeGridTagOption(id: 'hot', label: l10n.homeTagHot),
HomeGridTagOption(id: 'uplifting', label: l10n.homeTagUplifting),
HomeGridTagOption(id: 'sweet', label: l10n.homeTagSweet),
HomeGridTagOption(id: 'faceSlap', label: l10n.homeTagFaceSlap),
HomeGridTagOption(id: 'angst', label: l10n.homeTagAngst),
];
final categoryText = context
.read<CategoryController>()
.categories
.map((category) => category.name)
.toList(growable: false);
return definitions
.where((option) {
if (option.id == 'hot') {
return widget.feedController.dramas.any(isHotDrama);
}
return categoryText.any((text) => tagMatchesText(option.id, text)) ||
widget.feedController.dramas.any(
(drama) => matchesHomeTag(drama, option.id),
);
})
.toList(growable: false);
}
List<Drama> _filteredDramas(String? category, String? tag) {
final normalizedCategory = category?.toLowerCase();
return widget.feedController.dramas
.where((drama) {
final categoryMatches =
normalizedCategory == null ||
drama.category.toLowerCase().contains(normalizedCategory) ||
drama.genre.toLowerCase().contains(normalizedCategory);
final tagMatches = tag == null || matchesHomeTag(drama, tag);
return categoryMatches && tagMatches;
})
.toList(growable: false);
List<Drama> _dramasFor(
HomePrimaryTab tab,
String? tag,
List<HomeGridCategoryShortcut> shortcuts,
) {
Iterable<Drama> source = widget.feedController.dramas;
if (tab == HomePrimaryTab.costume || tab == HomePrimaryTab.community) {
final categoryName = shortcuts
.firstWhere((shortcut) => shortcut.tab == tab)
.categoryName
.toLowerCase();
if (categoryName.isEmpty) return const [];
source = source.where(
(drama) =>
drama.category.toLowerCase().contains(categoryName) ||
drama.genre.toLowerCase().contains(categoryName),
);
}
if (tag != null) {
source = source.where((drama) => matchesHomeTag(drama, tag));
}
final dramas = source.toList(growable: false);
switch (tab) {
case HomePrimaryTab.newReleases:
return [...dramas]..sort((a, b) {
final left = publishedAtForDrama(a);
final right = publishedAtForDrama(b);
if (left == null && right == null) return b.id.compareTo(a.id);
if (left == null) return 1;
if (right == null) return -1;
return right.compareTo(left);
});
case HomePrimaryTab.ranking:
return [...dramas]..sort((a, b) => b.viewCount.compareTo(a.viewCount));
case HomePrimaryTab.featured:
case HomePrimaryTab.categories:
case HomePrimaryTab.costume:
case HomePrimaryTab.community:
return dramas;
}
}
Widget _emptyState(AppLocalizations l10n, String? category, String? tag) {
Widget _emptyState(AppLocalizations l10n, HomePrimaryTab tab, String? tag) {
if (widget.feedController.lastError != null) {
return EmptyState(
key: const Key('home-grid-error'),
@@ -276,7 +311,10 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
onAction: () => unawaited(widget.feedController.refreshCurrent()),
);
}
final filtered = category != null || tag != null;
final filtered =
tab == HomePrimaryTab.costume ||
tab == HomePrimaryTab.community ||
tag != null;
return EmptyState(
key: const Key('home-grid-empty'),
icon: Icons.movie_filter_outlined,
@@ -284,10 +322,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
subtitle: filtered ? null : l10n.emptyHint,
actionLabel: filtered ? l10n.homeGridClearFilter : null,
onAction: filtered
? () => setState(() {
_selectedCategory = null;
_selectedTag = null;
})
? () => setState(() => _selectedTags[tab] = null)
: null,
);
}
+78 -61
View File
@@ -7,14 +7,25 @@ import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../l10n/app_localizations.dart';
enum HomePrimaryTab {
featured,
newReleases,
categories,
ranking,
costume,
community,
}
class HomeGridCategoryShortcut {
const HomeGridCategoryShortcut({
required this.label,
required this.categoryName,
required this.tab,
});
final String label;
final String categoryName;
final HomePrimaryTab tab;
}
class HomeGridTagOption {
@@ -29,18 +40,12 @@ class HomeGridCallbacks {
required this.onSearch,
required this.onOpenVip,
required this.onOpenWallet,
required this.onOpenNewReleases,
required this.onOpenCategories,
required this.onOpenRanking,
required this.onOpenFilter,
});
final VoidCallback onSearch;
final VoidCallback onOpenVip;
final VoidCallback onOpenWallet;
final VoidCallback onOpenNewReleases;
final VoidCallback onOpenCategories;
final VoidCallback onOpenRanking;
final VoidCallback onOpenFilter;
}
@@ -50,9 +55,9 @@ class HomeGridHeader extends StatelessWidget {
required this.callbacks,
required this.categories,
required this.tags,
required this.selectedCategory,
required this.selectedPrimary,
required this.selectedTag,
required this.onCategorySelected,
required this.onPrimarySelected,
required this.onTagSelected,
this.walletBadge,
});
@@ -60,9 +65,9 @@ class HomeGridHeader extends StatelessWidget {
final HomeGridCallbacks callbacks;
final List<HomeGridCategoryShortcut> categories;
final List<HomeGridTagOption> tags;
final String? selectedCategory;
final HomePrimaryTab selectedPrimary;
final String? selectedTag;
final ValueChanged<String?> onCategorySelected;
final ValueChanged<HomePrimaryTab> onPrimarySelected;
final ValueChanged<String?> onTagSelected;
final int? walletBadge;
@@ -71,6 +76,8 @@ class HomeGridHeader extends StatelessWidget {
final l10n = AppLocalizations.of(context)!;
return Column(
key: const Key('home-grid-header'),
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [_searchRow(l10n), _primaryRow(l10n), _tagRow(l10n)],
);
}
@@ -84,6 +91,8 @@ class HomeGridHeader extends StatelessWidget {
AppDimens.spaceSm,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: InkWell(
@@ -210,37 +219,38 @@ class HomeGridHeader extends StatelessWidget {
horizontal: AppDimens.feedPageHorizontal,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
_primaryItem(
key: const Key('home-section-featured'),
label: l10n.homeSectionFeatured,
selected: selectedCategory == null,
onTap: () => onCategorySelected(null),
tab: HomePrimaryTab.featured,
),
_primaryItem(
key: const Key('home-section-new'),
label: l10n.homeSectionNew,
selected: false,
onTap: callbacks.onOpenNewReleases,
tab: HomePrimaryTab.newReleases,
),
_primaryItem(
key: const Key('home-section-categories'),
label: l10n.homeSectionCategories,
selected: false,
onTap: callbacks.onOpenCategories,
tab: HomePrimaryTab.categories,
),
_primaryItem(
key: const Key('home-section-ranking'),
label: l10n.homeSectionRanking,
selected: false,
onTap: callbacks.onOpenRanking,
tab: HomePrimaryTab.ranking,
),
for (final category in categories)
_primaryItem(
key: Key('home-section-category-${category.categoryName}'),
key: Key(
category.categoryName.isEmpty
? 'home-section-${category.tab.name}'
: 'home-section-category-${category.categoryName}',
),
label: category.label,
selected: selectedCategory == category.categoryName,
onTap: () => onCategorySelected(category.categoryName),
tab: category.tab,
),
],
),
@@ -250,38 +260,35 @@ class HomeGridHeader extends StatelessWidget {
Widget _primaryItem({
required Key key,
required String label,
required bool selected,
required VoidCallback onTap,
required HomePrimaryTab tab,
}) {
final selected = selectedPrimary == tab;
return InkWell(
key: key,
onTap: onTap,
onTap: () => onPrimarySelected(tab),
child: Padding(
padding: const EdgeInsets.only(right: AppDimens.spaceXl),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
padding: const EdgeInsets.only(right: AppDimens.homePrimaryCategoryGap),
child: SizedBox(
height: AppDimens.homePrimaryRowHeight,
child: Align(
alignment: Alignment.bottomLeft,
child: AnimatedDefaultTextStyle(
key: Key('home-primary-label-${tab.name}'),
duration: AppDimens.homePrimaryTabAnimationDuration,
curve: Curves.easeOut,
style: TextStyle(
color: selected
? AppColors.navSelected
: AppColors.textTertiary,
fontSize: AppDimens.homePrimaryTabFont,
fontSize: selected
? AppDimens.homePrimaryTabSelectedFont
: AppDimens.homePrimaryTabFont,
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
letterSpacing: 0,
),
child: Text(label, textAlign: TextAlign.left),
),
const SizedBox(height: AppDimens.spaceXs),
Container(
height: AppDimens.homeSectionIndicator,
width: selected ? 20 : 0,
decoration: BoxDecoration(
color: AppColors.navSelected,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
),
],
),
),
),
);
@@ -296,16 +303,20 @@ class HomeGridHeader extends StatelessWidget {
AppDimens.spaceSm,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: SingleChildScrollView(
key: const Key('home-secondary-tags'),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (final tag in tags) ...[
_tagChip(tag),
const SizedBox(width: AppDimens.spaceSm),
const SizedBox(width: AppDimens.homeSecondaryTagGap),
],
],
),
@@ -339,29 +350,35 @@ class HomeGridHeader extends StatelessWidget {
Widget _tagChip(HomeGridTagOption tag) {
final selected = selectedTag == tag.id;
final label = Text(
tag.label,
style: TextStyle(
color: selected ? AppColors.navSelected : AppColors.textTertiary,
fontSize: AppDimens.homeSecondaryTagFont,
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
letterSpacing: 0,
),
);
return InkWell(
key: Key('home-tag-${tag.id}'),
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
onTap: () => onTagSelected(selected ? null : tag.id),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.spaceMd,
vertical: AppDimens.spaceSm,
),
decoration: BoxDecoration(
color: selected ? AppColors.contentSurface : AppColors.transparent,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
child: Text(
tag.label,
style: TextStyle(
color: selected ? AppColors.navSelected : AppColors.textTertiary,
fontSize: AppDimens.homeSecondaryTagFont,
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
letterSpacing: 0,
),
),
),
child: selected
? Container(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.spaceMd,
vertical: AppDimens.spaceSm,
),
decoration: BoxDecoration(
color: AppColors.contentSurface,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
child: label,
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceSm),
child: label,
),
);
}
}
@@ -36,6 +36,8 @@ class AllDramasFilterGrid extends StatefulWidget {
}
class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
String _region = 'all';
String _audio = 'all';
String? _genre;
bool _newest = false;
@@ -71,7 +73,11 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
icon: Icons.movie_filter_outlined,
title: l10n.homeGridFilterEmpty,
actionLabel: l10n.homeGridClearFilter,
onAction: () => setState(() => _genre = null),
onAction: () => setState(() {
_region = 'all';
_audio = 'all';
_genre = null;
}),
),
)
else
@@ -130,12 +136,28 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
child: Column(
key: const Key('all-dramas-filters'),
children: [
_chipRow(0, [l10n.all]),
_chipRow(1, [l10n.all]),
_chipRow(2, [
(id: null, label: l10n.all),
for (final genre in genres) (id: genre.id, label: genre.label),
]),
_selectableRow(
0,
[
(id: 'all', label: l10n.all),
(id: 'local', label: l10n.localFilter),
(id: 'asia', label: l10n.asiaFilter),
(id: 'western', label: l10n.westernFilter),
],
selected: _region,
onSelected: (value) => setState(() => _region = value),
),
_selectableRow(
1,
[
(id: 'all', label: l10n.all),
(id: 'subtitles', label: l10n.subtitlesOnlyFilter),
(id: 'dubbed', label: l10n.dubbedFilter),
],
selected: _audio,
onSelected: (value) => setState(() => _audio = value),
),
_genreRow(2, genres, l10n),
Row(
key: const Key('all-filter-row-3'),
children: [
@@ -173,7 +195,12 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
);
}
Widget _chipRow(int row, List<Object> options) {
Widget _selectableRow(
int row,
List<({String id, String label})> options, {
required String selected,
required ValueChanged<String> onSelected,
}) {
return Padding(
key: Key('all-filter-row-$row'),
padding: const EdgeInsets.only(bottom: AppDimens.spaceSm),
@@ -182,14 +209,12 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
child: Row(
children: [
for (final option in options) ...[
if (option is String)
_chip(label: option, selected: true, onTap: () {}),
if (option is ({String? id, String label}))
_chip(
label: option.label,
selected: _genre == option.id,
onTap: () => setState(() => _genre = option.id),
),
_chip(
key: Key('all-filter-${option.id}'),
label: option.label,
selected: selected == option.id,
onTap: () => onSelected(option.id),
),
const SizedBox(width: AppDimens.spaceSm),
],
],
@@ -198,6 +223,23 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
);
}
Widget _genreRow(
int row,
List<({String id, String label})> options,
AppLocalizations l10n,
) {
return _selectableRow(
row,
[
(id: 'all', label: l10n.all),
for (final genre in options) (id: genre.id, label: genre.label),
],
selected: _genre ?? 'all',
onSelected: (value) =>
setState(() => _genre = value == 'all' ? null : value),
);
}
Widget _chip({
Key? key,
required String label,
@@ -248,6 +290,7 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
}
List<Drama> _filtered() {
if (_region != 'all' || _audio != 'all') return const [];
final dramas = widget.feedController.dramas
.where((drama) {
return _genre == null || _matchesGenre(drama, _genre!, const []);
@@ -35,6 +35,7 @@ class RecommendSectionCard extends StatelessWidget {
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
@@ -83,6 +84,8 @@ class RecommendSectionCard extends StatelessWidget {
const SizedBox(height: AppDimens.cardCoverTitleGap),
Row(
key: Key('recommend-views-$sectionKey-${drama.id}'),
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Icon(
Icons.play_arrow_rounded,
@@ -123,38 +126,16 @@ class RecommendSectionCard extends StatelessWidget {
Widget _tags() {
final tags = _cardTags();
return Row(
children: [
for (final (index, tag) in tags.indexed)
Flexible(
child: Padding(
padding: EdgeInsets.only(
right: index == tags.length - 1 ? 0 : AppDimens.spaceXs,
),
child: Container(
key: Key('recommend-tag-$sectionKey-${drama.id}-$index'),
height: AppDimens.cardTagHeight,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.cardTagHorizontalPadding,
),
decoration: BoxDecoration(
color: AppColors.contentSurface,
borderRadius: BorderRadius.circular(AppDimens.cardTagRadius),
),
child: Text(
tag,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.cardTagFont,
),
),
),
),
),
],
return Text(
tags.join(' | '),
key: Key('recommend-tags-$sectionKey-${drama.id}'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.cardTagFont,
),
);
}
@@ -164,7 +145,7 @@ class RecommendSectionCard extends StatelessWidget {
.expand((value) => value.split(_tagSplitter))
.map((value) => value.trim())
.where((value) => value.isNotEmpty && seen.add(value))
.take(2)
.take(3)
.toList(growable: false);
}
}
+7 -7
View File
@@ -79,14 +79,14 @@ void main() {
);
expect(views.style?.fontSize, AppDimens.cardViewsFont);
expect(views.style?.color, AppColors.textPrimary);
final tag = find.byKey(const Key('following-tag-the-signal-demo-0'));
expect(tag, findsOneWidget);
expect(tester.getSize(tag).height, AppDimens.cardTagHeight);
final tagText = tester.widget<Text>(
find.descendant(of: tag, matching: find.byType(Text)),
final tags = tester.widget<Text>(
find.byKey(const Key('following-tags-the-signal-demo')),
);
expect(tagText.style?.fontSize, AppDimens.cardTagFont);
expect(tagText.style?.color, AppColors.textTertiary);
expect(tags.data, 'Suspense | Mystery');
expect(tags.style?.fontSize, AppDimens.cardTagFont);
expect(tags.style?.color, AppColors.textTertiary);
expect(tester.getTopLeft(title).dx, tester.getTopLeft(cover).dx);
expect(tester.getTopLeft(tags).dx, tester.getTopLeft(cover).dx);
await tester.tap(find.byKey(const Key('following-edit')));
await tester.pumpAndSettle();
+16 -23
View File
@@ -26,16 +26,17 @@ void main() {
);
final card = find.byKey(const Key('home-feed-card-anime-card'));
final container = tester.widget<Container>(card);
final decoration = container.decoration! as BoxDecoration;
expect(decoration.color, AppColors.contentCard);
expect(decoration.boxShadow, isNull);
expect(tester.widget<Material>(card).color, AppColors.transparent);
expect(
decoration.borderRadius,
BorderRadius.circular(AppDimens.homeCardRadius),
tester
.widgetList<Container>(
find.descendant(of: card, matching: find.byType(Container)),
)
.map((container) => container.decoration)
.whereType<BoxDecoration>()
.every((decoration) => decoration.border == null),
isTrue,
);
expect((decoration.border! as Border).top.width, 1);
expect((decoration.border! as Border).top.color, AppColors.contentBorder);
final cover = find.byKey(const Key('home-feed-cover-anime-card'));
final coverSize = tester.getSize(cover);
@@ -70,22 +71,14 @@ void main() {
expect(title.style?.fontSize, AppDimens.cardTitleFont);
expect(title.style?.fontWeight, FontWeight.w500);
expect(title.style?.height, AppDimens.cardTitleLineHeight);
final firstTag = find.byKey(const Key('home-feed-tag-anime-card-0'));
expect(firstTag, findsOneWidget);
expect(find.byKey(const Key('home-feed-tag-anime-card-1')), findsOneWidget);
expect(find.text('Romance'), findsOneWidget);
expect(find.text('Urban'), findsOneWidget);
final tagContainer = tester.widget<Container>(firstTag);
expect(tester.getSize(firstTag).height, AppDimens.cardTagHeight);
expect(
(tagContainer.decoration! as BoxDecoration).borderRadius,
BorderRadius.circular(AppDimens.cardTagRadius),
final tags = tester.widget<Text>(
find.byKey(const Key('home-feed-tags-anime-card')),
);
final tagText = tester.widget<Text>(
find.descendant(of: firstTag, matching: find.byType(Text)),
);
expect(tagText.style?.fontSize, AppDimens.cardTagFont);
expect(tagText.style?.color, AppColors.textTertiary);
expect(tags.data, 'Romance | Urban');
expect(tags.style?.fontSize, AppDimens.cardTagFont);
expect(tags.style?.color, AppColors.textTertiary);
expect(tester.getTopLeft(title).dx, tester.getTopLeft(cover).dx);
expect(tester.getTopLeft(tags).dx, tester.getTopLeft(cover).dx);
await tester.tap(card);
expect(taps, 1);
+70 -14
View File
@@ -11,6 +11,7 @@ import 'package:fruitshorts/core/data/repositories/catalog_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/pages/home/widgets/home_feed_card.dart';
import 'package:fruitshorts/pages/player/player_view.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';
@@ -88,29 +89,82 @@ void main() {
expect(find.byKey(const Key('home-tag-uplifting')), findsNothing);
});
testWidgets('should_open_existing_pages_from_home_sections', (tester) async {
testWidgets('should_switch_home_sections_without_pushing_pages', (
tester,
) async {
final repository = _PagedCatalogRepository();
await _pumpApp(tester, repository);
String firstDramaId() {
return tester
.widget<HomeFeedCard>(find.byType(HomeFeedCard).first)
.drama
.id;
}
expect(find.byKey(const Key('home-grid')), findsOneWidget);
expect(firstDramaId(), 'page1-0');
await tester.tap(find.byKey(const Key('home-section-new')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('new-releases-page')), findsOneWidget);
await tester.pageBack();
await tester.pumpAndSettle();
expect(find.byKey(const Key('new-releases-page')), findsNothing);
expect(firstDramaId(), 'page1-9');
await tester.ensureVisible(find.byKey(const Key('home-section-ranking')));
await tester.tap(find.byKey(const Key('home-section-ranking')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('ranking-page')), findsOneWidget);
await tester.pageBack();
await tester.pumpAndSettle();
expect(find.byKey(const Key('ranking-page')), findsNothing);
expect(firstDramaId(), 'page1-0');
await tester.ensureVisible(
find.byKey(const Key('home-section-categories')),
);
await tester.tap(find.byKey(const Key('home-section-categories')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('filter-page')), findsOneWidget);
expect(find.byKey(const Key('filter-page')), findsNothing);
expect(find.byKey(const Key('home-grid-cards')), findsOneWidget);
await tester.tap(find.byKey(const Key('home-section-category-古装')));
await tester.pumpAndSettle();
expect(firstDramaId(), 'page1-0');
await tester.tap(find.byKey(const Key('home-section-category-社群')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('home-grid-empty')), findsOneWidget);
expect(find.byKey(const Key('filter-page')), findsNothing);
});
testWidgets('should_keep_scroll_position_when_switching_sections', (
tester,
) async {
final repository = _PagedCatalogRepository();
await _pumpApp(tester, repository);
await tester.drag(
find.byKey(const Key('home-grid-scroll')),
const Offset(0, -320),
);
await tester.pumpAndSettle();
final scrollable = find
.descendant(
of: find.byKey(const Key('home-grid-scroll')),
matching: find.byType(Scrollable),
)
.first;
final before = tester.state<ScrollableState>(scrollable).position.pixels;
expect(before, greaterThan(0));
await tester.tap(find.byKey(const Key('home-section-new')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('home-section-featured')));
await tester.pumpAndSettle();
final afterScrollable = find
.descendant(
of: find.byKey(const Key('home-grid-scroll')),
matching: find.byType(Scrollable),
)
.first;
expect(
tester.state<ScrollableState>(afterScrollable).position.pixels,
closeTo(before, 0.1),
);
});
testWidgets('should_load_next_page_without_expanding_page_size', (
@@ -232,14 +286,16 @@ class _PagedCatalogRepository implements CatalogRepository {
}
Drama _drama(String id) {
final index = int.tryParse(id.split('-').last) ?? 0;
return Drama(
id: id,
title: 'Grid Drama $id',
synopsis: 'synopsis',
posterUrl: '',
genre: 'Romance',
category: 'Urban',
viewCount: 200000,
category: index.isEven ? '古装' : 'Urban',
viewCount: (10 - index) * 10000,
createdAt: DateTime(2026, 10, 1).add(Duration(days: index)),
detailLoaded: true,
episodes: [
Episode(
+44 -18
View File
@@ -109,23 +109,29 @@ void main() {
);
expect(placeholder.style?.fontSize, AppDimens.searchPlaceholderFont);
expect(placeholder.style?.color, AppColors.textSecondary);
final selectedPrimary = tester.widget<Text>(
final selectedPrimary = tester.widget<AnimatedDefaultTextStyle>(
find.byKey(const Key('home-primary-label-featured')),
);
final unselectedPrimary = tester.widget<AnimatedDefaultTextStyle>(
find.byKey(const Key('home-primary-label-newReleases')),
);
expect(
selectedPrimary.style.fontSize,
AppDimens.homePrimaryTabSelectedFont,
);
expect(selectedPrimary.style.fontWeight, FontWeight.w700);
expect(selectedPrimary.style.color, AppColors.navSelected);
expect(selectedPrimary.duration, AppDimens.homePrimaryTabAnimationDuration);
expect(unselectedPrimary.style.fontSize, AppDimens.homePrimaryTabFont);
expect(unselectedPrimary.style.fontWeight, FontWeight.w400);
expect(unselectedPrimary.style.color, AppColors.textTertiary);
expect(
find.descendant(
of: find.byKey(const Key('home-section-featured')),
matching: find.text('Featured'),
matching: find.byType(Container),
),
findsNothing,
);
final unselectedPrimary = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('home-section-new')),
matching: find.text('New'),
),
);
expect(selectedPrimary.style?.fontSize, AppDimens.homePrimaryTabFont);
expect(selectedPrimary.style?.fontWeight, FontWeight.w700);
expect(selectedPrimary.style?.color, AppColors.navSelected);
expect(unselectedPrimary.style?.fontWeight, FontWeight.w400);
expect(unselectedPrimary.style?.color, AppColors.textTertiary);
final secondaryTag = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('home-tag-hot')),
@@ -135,18 +141,38 @@ void main() {
expect(secondaryTag.style?.fontSize, AppDimens.homeSecondaryTagFont);
expect(secondaryTag.style?.fontWeight, FontWeight.w400);
expect(secondaryTag.style?.color, AppColors.textTertiary);
final unselectedTagChip = tester.widget<Container>(
expect(
find.descendant(
of: find.byKey(const Key('home-tag-hot')),
matching: find.byType(Container),
),
findsNothing,
);
final primaryStart = tester.getTopLeft(find.text('Featured'));
final pageStart = tester.getTopLeft(searchBox);
expect(primaryStart.dx, pageStart.dx);
final primaryGap =
tester.getTopLeft(find.text('New')).dx -
tester.getBottomRight(find.text('Featured')).dx;
expect(primaryGap, closeTo(AppDimens.homePrimaryCategoryGap, 0.1));
final tagStart = tester.getTopLeft(find.text('Hot'));
expect(tagStart.dx, pageStart.dx);
final tagGap =
tester.getTopLeft(find.text('Uplifting')).dx -
tester.getBottomRight(find.text('Hot')).dx;
expect(tagGap, closeTo(AppDimens.homeSecondaryTagGap, 0.1));
await tester.tap(find.byKey(const Key('home-tag-hot')));
await tester.pump();
final selectedTagChip = tester.widget<Container>(
find.descendant(
of: find.byKey(const Key('home-tag-hot')),
matching: find.byType(Container),
),
);
expect(
(unselectedTagChip.decoration! as BoxDecoration).color,
AppColors.transparent,
(selectedTagChip.decoration! as BoxDecoration).color,
AppColors.contentSurface,
);
await tester.tap(find.byKey(const Key('home-tag-hot')));
await tester.pump();
final selectedTag = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('home-tag-hot')),
+14 -13
View File
@@ -103,18 +103,14 @@ void main() {
);
expect(views.style?.fontSize, AppDimens.cardViewsFont);
expect(views.style?.color, AppColors.textPrimary);
final tag = find.byKey(const Key('recommend-tag-hits-the-signal-demo-0'));
expect(tag, findsOneWidget);
expect(tester.getSize(tag).height, AppDimens.cardTagHeight);
final tagText = tester.widget<Text>(
find.descendant(of: tag, matching: find.byType(Text)),
);
expect(tagText.style?.fontSize, AppDimens.cardTagFont);
expect(tagText.style?.color, AppColors.textTertiary);
expect(
(tester.widget<Container>(tag).decoration! as BoxDecoration).borderRadius,
BorderRadius.circular(AppDimens.cardTagRadius),
final tags = tester.widget<Text>(
find.byKey(const Key('recommend-tags-hits-the-signal-demo')),
);
expect(tags.data, 'Suspense | Mystery');
expect(tags.style?.fontSize, AppDimens.cardTagFont);
expect(tags.style?.color, AppColors.textTertiary);
expect(tester.getTopLeft(cardTitle).dx, tester.getTopLeft(cover).dx);
expect(tester.getTopLeft(tags).dx, tester.getTopLeft(cover).dx);
});
testWidgets('should_switch_inner_tabs_without_losing_grid_filter_state', (
@@ -132,8 +128,13 @@ void main() {
for (var row = 0; row < 4; row++) {
expect(find.byKey(Key('all-filter-row-$row')), findsOneWidget);
}
expect(find.byKey(const Key('all-filter-local')), findsNothing);
expect(find.byKey(const Key('all-filter-subtitles')), findsNothing);
expect(find.byKey(const Key('all-filter-local')), findsOneWidget);
expect(find.byKey(const Key('all-filter-subtitles')), findsOneWidget);
await tester.tap(find.byKey(const Key('all-filter-local')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('all-dramas-empty')), findsOneWidget);
await tester.tap(find.byKey(const Key('all-filter-all')).first);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('all-sort-new')));
await tester.pump();
expect(_textStyle(tester, 'New').fontWeight, FontWeight.w700);