feat(app): 客户端展示多语言并支持筛选分页

This commit is contained in:
Zhou Weidong committed 2026-10-24 17:20:24 +08:00
1 parent 8481611372
commit 8b50b5bd06
68 files changed
+2742 -1120

No files matched your search

@@ -0,0 +1,178 @@
// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/data/models/drama.dart';
import '../../../core/data/services/category_controller.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/app_pull_to_refresh.dart';
import '../../../core/widgets/drama_grid_skeleton.dart';
import '../../../core/widgets/empty_state.dart';
import '../../../core/widgets/paged_list_footer.dart';
import '../../../l10n/app_localizations.dart';
import 'home_feed_card.dart';
const double _loadMoreThreshold = 240;
/// 首页分类下的短剧:按分类分页,滑到底加载下一页。
class HomeCategoryDramas extends StatefulWidget {
const HomeCategoryDramas({
super.key,
required this.onOpenDrama,
this.lockedCategoryId,
});
final ValueChanged<Drama> onOpenDrama;
final String? lockedCategoryId;
@override
State<HomeCategoryDramas> createState() => _HomeCategoryDramasState();
}
class _HomeCategoryDramasState extends State<HomeCategoryDramas> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _ensureOpened());
}
@override
void didUpdateWidget(HomeCategoryDramas oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.lockedCategoryId != widget.lockedCategoryId) {
_ensureOpened();
}
}
void _ensureOpened() {
if (!mounted) return;
final controller = context.read<CategoryController>();
final locked = widget.lockedCategoryId;
if (locked == null || locked.isEmpty) return;
final slot = controller.slotOf(locked);
if (slot.loading || slot.pageIndex > 0) return;
unawaited(controller.openCategory(locked));
}
bool _onScroll(ScrollNotification notification) {
final categoryId = widget.lockedCategoryId;
if (categoryId == null || categoryId.isEmpty) return false;
if (notification.depth != 0) return false;
if (notification.metrics.extentAfter > _loadMoreThreshold) return false;
if (notification is! ScrollUpdateNotification &&
notification is! ScrollEndNotification &&
notification is! OverscrollNotification) {
return false;
}
unawaited(context.read<CategoryController>().loadMore(categoryId));
return false;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final controller = context.watch<CategoryController>();
final categoryId = widget.lockedCategoryId ?? '';
final slot = categoryId.isEmpty ? null : controller.slotOf(categoryId);
final dramas = slot?.dramas ?? const <Drama>[];
return Column(
children: [
Expanded(
child: NotificationListener<ScrollNotification>(
onNotification: _onScroll,
child: AppPullToRefresh(
onRefresh: () async {
if (categoryId.isNotEmpty) {
await controller.openCategory(categoryId);
}
},
child: _body(context, l10n, controller, categoryId, dramas),
),
),
),
],
);
}
Widget _body(
BuildContext context,
AppLocalizations l10n,
CategoryController controller,
String categoryId,
List<Drama> dramas,
) {
final slot = categoryId.isEmpty ? null : controller.slotOf(categoryId);
if (slot != null && slot.loading && dramas.isEmpty) {
return const DramaGridSkeleton(
key: Key('home-grid-skeleton'),
columns: 3,
horizontalPadding: AppDimens.feedPageHorizontal,
crossAxisSpacing: AppDimens.homeGridGap,
mainAxisSpacing: AppDimens.homeGridRowGap,
);
}
if (dramas.isEmpty) {
return EmptyState(
key: const Key('home-grid-empty'),
icon: Icons.movie_filter_outlined,
title: l10n.emptyContent,
);
}
return LayoutBuilder(
builder: (context, constraints) {
final itemWidth =
(constraints.maxWidth -
AppDimens.feedPageHorizontal * 2 -
AppDimens.homeGridGap * 2) /
3;
final itemExtent =
itemWidth / AppDimens.cardCoverAspectRatio +
AppDimens.homeCardBodyExtent;
return CustomScrollView(
key: const Key('home-grid-scroll'),
physics: AppPullToRefresh.physics,
slivers: [
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,
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
child: PagedListFooter(
loading: slot != null && slot.loadingMore,
hasMore: slot?.hasMore ?? false,
hasItems: dramas.isNotEmpty,
errorMessage: null,
onRetry: () => context.read<CategoryController>().loadMore(
categoryId,
),
),
),
),
],
);
},
);
}
}
+9 -6
View File
@@ -99,9 +99,9 @@ class HomeDramaCard extends StatelessWidget {
children: [
_cover(),
const SizedBox(height: _coverTitleGap),
_title(),
_title(context),
const SizedBox(height: _lineGap),
_genre(),
_genre(context),
const SizedBox(height: _lineGap),
_metadata(),
],
@@ -211,9 +211,9 @@ class HomeDramaCard extends StatelessWidget {
);
}
Widget _title() {
Widget _title(BuildContext context) {
return Text(
drama.title,
drama.localizedTitle(Localizations.localeOf(context).languageCode),
key: Key('home-drama-title-${drama.id}'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -225,8 +225,11 @@ class HomeDramaCard extends StatelessWidget {
);
}
Widget _genre() {
final genre = drama.genre.isEmpty ? drama.category : drama.genre;
Widget _genre(BuildContext context) {
final labels = drama.localizedTypeLabels(Localizations.localeOf(context).languageCode);
final genre = labels.isEmpty
? (drama.genre.isEmpty ? drama.category : drama.genre)
: labels.join(' / ');
return Text(
genre.isEmpty ? l10n.videoPending : genre,
key: Key('home-drama-genre-${drama.id}'),
+14 -46
View File
@@ -8,28 +8,21 @@ import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/cover_play_count.dart';
import '../../../core/widgets/drama_cover_badge_ribbon.dart';
import '../../../core/widgets/remote_image.dart';
import '../../../l10n/app_localizations.dart';
import 'home_drama_classification.dart';
final RegExp _tagSplitter = RegExp(r'[,/、·]');
enum HomeDramaBadge { hot, newRelease, anime }
class HomeFeedCard extends StatelessWidget {
const HomeFeedCard({
super.key,
required this.drama,
required this.onTap,
this.now,
});
final Drama drama;
final VoidCallback onTap;
final DateTime? now;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Material(
key: Key('home-feed-card-${drama.id}'),
color: AppColors.transparent,
@@ -40,7 +33,7 @@ class HomeFeedCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
_cover(l10n),
_cover(context),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
@@ -51,7 +44,7 @@ class HomeFeedCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
drama.title,
drama.localizedTitle(Localizations.localeOf(context).languageCode),
key: Key('home-feed-title-${drama.id}'),
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -64,7 +57,7 @@ class HomeFeedCard extends StatelessWidget {
),
),
const SizedBox(height: AppDimens.cardTitleTagGap),
_tags(),
_tags(context),
],
),
),
@@ -75,8 +68,8 @@ class HomeFeedCard extends StatelessWidget {
);
}
Widget _cover(AppLocalizations l10n) {
final badge = _badge();
Widget _cover(BuildContext context) {
final label = drama.localizedTag(Localizations.localeOf(context).languageCode);
return ClipRRect(
key: Key('home-feed-cover-clip-${drama.id}'),
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
@@ -92,14 +85,14 @@ class HomeFeedCard extends StatelessWidget {
showFallbackIcon: true,
fallbackColor: AppColors.contentSurface,
),
if (badge != null)
if (label.isNotEmpty)
Positioned(
top: 0,
right: 0,
child: DramaCoverBadgeRibbon(
label: _badgeLabel(l10n, badge),
color: _badgeColor(badge),
keyPrefix: '${drama.id}-${badge.name}',
label: label,
color: drama.tagRibbonColor(AppColors.badgeHot),
keyPrefix: '${drama.id}-tag',
),
),
const Positioned(
@@ -122,35 +115,8 @@ class HomeFeedCard extends StatelessWidget {
);
}
HomeDramaBadge? _badge() {
if (drama.dramaType.toUpperCase() == 'ANIME') {
return HomeDramaBadge.anime;
}
if (isNewDrama(drama, now: now)) return HomeDramaBadge.newRelease;
if (isHotDrama(drama)) {
return HomeDramaBadge.hot;
}
return null;
}
String _badgeLabel(AppLocalizations l10n, HomeDramaBadge badge) {
return switch (badge) {
HomeDramaBadge.hot => l10n.homeBadgeHot,
HomeDramaBadge.newRelease => l10n.homeBadgeNew,
HomeDramaBadge.anime => l10n.homeBadgeAnime,
};
}
Color _badgeColor(HomeDramaBadge badge) {
return switch (badge) {
HomeDramaBadge.hot => AppColors.badgeHot,
HomeDramaBadge.newRelease => AppColors.badgeNew,
HomeDramaBadge.anime => AppColors.badgeAnime,
};
}
Widget _tags() {
final tags = _cardTags();
Widget _tags(BuildContext context) {
final tags = _cardTags(context);
if (tags.isEmpty) return const SizedBox.shrink();
return Text(
tags.join(' | '),
@@ -166,7 +132,9 @@ class HomeFeedCard extends StatelessWidget {
);
}
List<String> _cardTags() {
List<String> _cardTags(BuildContext context) {
final localized = drama.localizedTypeLabels(Localizations.localeOf(context).languageCode);
if (localized.isNotEmpty) return localized.take(3).toList(growable: false);
final seen = <String>{};
return [drama.genre, drama.category]
.expand((value) => value.split(_tagSplitter))
+89 -116
View File
@@ -14,20 +14,16 @@ import '../../../core/data/services/home_feed_controller.dart';
import '../../../core/data/services/wallet_controller.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/app_pull_to_refresh.dart';
import '../../../core/widgets/drama_grid_skeleton.dart';
import '../../../core/widgets/empty_state.dart';
import '../../../core/widgets/paged_list_footer.dart';
import '../../../l10n/app_localizations.dart';
import '../../ranking/ranking_page.dart';
import 'home_drama_classification.dart';
import 'home_category_dramas.dart';
import 'home_feed_card.dart';
import 'home_grid_header.dart';
const double _loadMoreThreshold = 240;
const List<String> _costumeAliases = ['古装', 'costume', 'period'];
const List<String> _communityAliases = ['社群', 'community', 'komunitas'];
class HomeFeedGrid extends StatefulWidget {
const HomeFeedGrid({
@@ -46,21 +42,15 @@ class HomeFeedGrid extends StatefulWidget {
}
class _HomeFeedGridState extends State<HomeFeedGrid> {
HomePrimaryTab _selectedPrimary = HomePrimaryTab.featured;
int _sectionIndex = 0;
final PageController _pageController = PageController();
final Map<HomePrimaryTab, ScrollController> _scrollControllers = {
for (final tab in HomePrimaryTab.values) tab: ScrollController(),
};
final Map<HomePrimaryTab, String?> _selectedTags = {
for (final tab in HomePrimaryTab.values) tab: null,
};
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _pageController.hasClients) {
_pageController.jumpToPage(_selectedPrimary.index);
_pageController.jumpToPage(_sectionIndex);
}
});
}
@@ -68,9 +58,6 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
@override
void dispose() {
_pageController.dispose();
for (final controller in _scrollControllers.values) {
controller.dispose();
}
super.dispose();
}
@@ -81,17 +68,16 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
final catalog = context.watch<CatalogController>();
final auth = context.watch<AuthController>();
final wallet = context.watch<WalletController>();
final shortcuts = _categoryShortcuts(categories, l10n);
final shortcuts = _categoryShortcuts(
categories,
Localizations.localeOf(context).languageCode,
);
final walletBadge = auth.isSignedIn && wallet.snapshot != null
? wallet.balance
: null;
final ranking = _selectedPrimary == HomePrimaryTab.ranking;
return DecoratedBox(
key: const Key('home-grid'),
decoration: BoxDecoration(
color: ranking ? null : AppColors.appBackground,
gradient: ranking ? AppTheme.pageGradient : null,
),
decoration: const BoxDecoration(color: AppColors.appBackground),
child: SafeArea(
bottom: false,
child: Column(
@@ -100,8 +86,8 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
HomeGridHeader(
callbacks: widget.callbacks,
categories: shortcuts,
selectedPrimary: _selectedPrimary,
onPrimarySelected: _selectPrimary,
selectedIndex: _sectionIndex,
onSectionSelected: _selectSection,
walletBadge: walletBadge,
),
Expanded(
@@ -110,22 +96,29 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
builder: (context, _) => PageView.builder(
key: const Key('home-primary-page-view'),
controller: _pageController,
itemCount: HomePrimaryTab.values.length,
onPageChanged: (index) => setState(
() => _selectedPrimary = HomePrimaryTab.values[index],
),
itemBuilder: (context, index) => _KeepAlivePage(
key: PageStorageKey(
'home-primary-page-${HomePrimaryTab.values[index].name}',
),
child: _tabGrid(
context,
l10n,
tab: HomePrimaryTab.values[index],
catalog: catalog,
shortcuts: shortcuts,
),
),
itemCount: shortcuts.isEmpty ? 1 : shortcuts.length,
onPageChanged: (index) =>
setState(() => _sectionIndex = index),
itemBuilder: (context, index) {
if (shortcuts.isEmpty) {
return _categoryPlaceholder(
l10n,
context.watch<CategoryController>(),
);
}
final category = shortcuts[index];
return _KeepAlivePage(
key: PageStorageKey(
'home-primary-page-${category.categoryId}',
),
child: _tabGrid(
context,
l10n,
category: category,
catalog: catalog,
),
);
},
),
),
),
@@ -135,28 +128,54 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
);
}
void _selectPrimary(HomePrimaryTab tab) {
if (tab == _selectedPrimary) return;
void _selectSection(int index) {
if (index == _sectionIndex) return;
if (!_pageController.hasClients) {
setState(() => _selectedPrimary = tab);
setState(() => _sectionIndex = index);
return;
}
unawaited(
_pageController.animateToPage(
tab.index,
index,
duration: AppDimens.homePrimaryTabAnimationDuration,
curve: Curves.easeOut,
),
);
}
Widget _categoryPlaceholder(
AppLocalizations l10n,
CategoryController controller,
) {
if (controller.loading) {
return const DramaGridSkeleton(
key: Key('home-grid-skeleton'),
columns: 3,
horizontalPadding: AppDimens.feedPageHorizontal,
crossAxisSpacing: AppDimens.homeGridGap,
mainAxisSpacing: AppDimens.homeGridRowGap,
);
}
return EmptyState(
key: const Key('home-grid-empty'),
icon: Icons.movie_filter_outlined,
title: l10n.emptyContent,
);
}
Widget _tabGrid(
BuildContext context,
AppLocalizations l10n, {
required HomePrimaryTab tab,
required HomeGridCategoryShortcut category,
required CatalogController catalog,
required List<HomeGridCategoryShortcut> shortcuts,
}) {
final controller = context.read<CategoryController>();
if (controller.dramaPageLoader != null && category.categoryId.isNotEmpty) {
return HomeCategoryDramas(
lockedCategoryId: category.categoryId,
onOpenDrama: widget.onOpenDrama,
);
}
if (catalog.error != null && widget.feedController.dramas.isEmpty) {
return _catalogErrorState(l10n, catalog);
}
@@ -170,11 +189,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
mainAxisSpacing: AppDimens.homeGridRowGap,
);
}
if (tab == HomePrimaryTab.ranking) {
return const RankingPage(embedded: true);
}
final tag = _selectedTags[tab];
final dramas = _dramasFor(tab, tag, shortcuts);
final dramas = _dramasFor(category);
if (widget.feedController.loadingFirstPage && dramas.isEmpty) {
return const DramaGridSkeleton(
key: Key('home-grid-skeleton'),
@@ -201,7 +216,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
onRefresh: _refresh,
child: CustomScrollView(
key: const Key('home-grid-scroll'),
controller: _scrollControllers[tab],
controller: null,
physics: AppPullToRefresh.physics,
slivers: [
if (dramas.isEmpty)
@@ -237,6 +252,8 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
child: PagedListFooter(
loading: widget.feedController.loadingMore,
hasMore: widget.feedController.hasMore,
hasItems: true,
errorMessage: widget.feedController.lastError == null
? null
: l10n.catalogError,
@@ -289,75 +306,31 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
List<HomeGridCategoryShortcut> _categoryShortcuts(
List<ContentCategory> categories,
AppLocalizations l10n,
String languageCode,
) {
final costume = _findCategory(categories, _costumeAliases);
final community = _findCategory(categories, _communityAliases);
return [
HomeGridCategoryShortcut(
label: l10n.homeSectionCostume,
categoryName: costume?.name ?? '',
tab: HomePrimaryTab.costume,
),
HomeGridCategoryShortcut(
label: l10n.homeSectionCommunity,
categoryName: community?.name ?? '',
tab: HomePrimaryTab.community,
),
for (final category in categories)
HomeGridCategoryShortcut(
label: category.localizedName(languageCode),
categoryName: category.name,
categoryId: category.id,
tab: HomePrimaryTab.categories,
),
];
}
ContentCategory? _findCategory(
List<ContentCategory> categories,
List<String> aliases,
) {
for (final category in categories) {
final name = category.name.toLowerCase();
if (aliases.any(name.contains)) return category;
}
return null;
}
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;
}
List<Drama> _dramasFor(HomeGridCategoryShortcut category) {
final dramas = widget.feedController.dramas;
final categoryName = category.categoryName.toLowerCase();
if (categoryName.isEmpty) return dramas;
final matched = dramas
.where(
(drama) =>
drama.category.toLowerCase().contains(categoryName) ||
drama.genre.toLowerCase().contains(categoryName),
)
.toList(growable: false);
return matched.isEmpty ? dramas : matched;
}
Widget _emptyState(AppLocalizations l10n) {
+84 -47
View File
@@ -20,11 +20,13 @@ class HomeGridCategoryShortcut {
const HomeGridCategoryShortcut({
required this.label,
required this.categoryName,
required this.categoryId,
required this.tab,
});
final String label;
final String categoryName;
final String categoryId;
final HomePrimaryTab tab;
}
@@ -47,15 +49,15 @@ class HomeGridHeader extends StatelessWidget {
super.key,
required this.callbacks,
required this.categories,
required this.selectedPrimary,
required this.onPrimarySelected,
required this.selectedIndex,
required this.onSectionSelected,
this.walletBadge,
});
final HomeGridCallbacks callbacks;
final List<HomeGridCategoryShortcut> categories;
final HomePrimaryTab selectedPrimary;
final ValueChanged<HomePrimaryTab> onPrimarySelected;
final int selectedIndex;
final ValueChanged<int> onSectionSelected;
final int? walletBadge;
@override
@@ -67,7 +69,7 @@ class HomeGridHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
children: [
_searchRow(l10n),
_primaryRow(l10n),
_primaryRow(),
const SizedBox(height: AppDimens.homeSecondaryGridGap),
],
);
@@ -225,7 +227,73 @@ class HomeGridHeader extends StatelessWidget {
);
}
Widget _primaryRow(AppLocalizations l10n) {
Widget _primaryRow() {
return _CategoryBar(
categories: categories,
selectedIndex: selectedIndex,
onSectionSelected: onSectionSelected,
);
}
}
class _CategoryBar extends StatefulWidget {
const _CategoryBar({
required this.categories,
required this.selectedIndex,
required this.onSectionSelected,
});
final List<HomeGridCategoryShortcut> categories;
final int selectedIndex;
final ValueChanged<int> onSectionSelected;
@override
State<_CategoryBar> createState() => _CategoryBarState();
}
class _CategoryBarState extends State<_CategoryBar> {
final List<GlobalKey> _itemKeys = [];
@override
void initState() {
super.initState();
_syncKeys();
WidgetsBinding.instance.addPostFrameCallback((_) => _revealSelected());
}
@override
void didUpdateWidget(_CategoryBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.categories.length != widget.categories.length) {
_syncKeys();
}
if (oldWidget.selectedIndex != widget.selectedIndex ||
oldWidget.categories.length != widget.categories.length) {
WidgetsBinding.instance.addPostFrameCallback((_) => _revealSelected());
}
}
void _syncKeys() {
_itemKeys
..clear()
..addAll(List.generate(widget.categories.length, (_) => GlobalKey()));
}
void _revealSelected() {
final index = widget.selectedIndex;
if (!mounted || index < 0 || index >= _itemKeys.length) return;
final target = _itemKeys[index].currentContext;
if (target == null) return;
Scrollable.ensureVisible(
target,
alignment: 0.5,
duration: AppDimens.homePrimaryTabAnimationDuration,
curve: Curves.easeOut,
);
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
key: const Key('home-primary-sections'),
scrollDirection: Axis.horizontal,
@@ -234,60 +302,29 @@ class HomeGridHeader extends StatelessWidget {
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
_primaryItem(
key: const Key('home-section-featured'),
label: l10n.homeSectionFeatured,
tab: HomePrimaryTab.featured,
),
_primaryItem(
key: const Key('home-section-new'),
label: l10n.homeSectionNew,
tab: HomePrimaryTab.newReleases,
),
_primaryItem(
key: const Key('home-section-categories'),
label: l10n.homeSectionCategories,
tab: HomePrimaryTab.categories,
),
_primaryItem(
key: const Key('home-section-ranking'),
label: l10n.homeSectionRanking,
tab: HomePrimaryTab.ranking,
),
for (final category in categories)
_primaryItem(
key: Key(
category.categoryName.isEmpty
? 'home-section-${category.tab.name}'
: 'home-section-category-${category.categoryName}',
),
label: category.label,
tab: category.tab,
),
for (var index = 0; index < widget.categories.length; index++)
_item(index),
],
),
);
}
Widget _primaryItem({
required Key key,
required String label,
required HomePrimaryTab tab,
}) {
final selected = selectedPrimary == tab;
Widget _item(int index) {
final category = widget.categories[index];
final selected = widget.selectedIndex == index;
return InkWell(
key: key,
onTap: () => onPrimarySelected(tab),
key: _itemKeys[index],
onTap: () => widget.onSectionSelected(index),
child: Padding(
key: Key('home-section-category-${category.categoryName}'),
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}'),
key: Key('home-primary-label-${category.categoryId}'),
duration: AppDimens.homePrimaryTabAnimationDuration,
curve: Curves.easeOut,
style: TextStyle(
@@ -300,7 +337,7 @@ class HomeGridHeader extends StatelessWidget {
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
letterSpacing: 0,
),
child: Text(label, textAlign: TextAlign.left),
child: Text(category.label, textAlign: TextAlign.left),
),
),
),