// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../../core/formatting/count_format.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/widgets/animated_nav_label.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.categoryId, required this.tab, this.layoutType = 'GRID', }); final String label; final String categoryName; final String categoryId; final HomePrimaryTab tab; final String layoutType; bool get isRanking => layoutType == 'RANKING'; } class HomeGridCallbacks { const HomeGridCallbacks({ required this.onSearch, required this.onOpenVip, required this.onOpenWallet, required this.onOpenFilter, }); final VoidCallback onSearch; final VoidCallback onOpenVip; final VoidCallback onOpenWallet; final VoidCallback onOpenFilter; } class HomeGridHeader extends StatelessWidget { const HomeGridHeader({ super.key, required this.callbacks, required this.categories, required this.selectedIndex, required this.onSectionSelected, this.walletBadge, }); final HomeGridCallbacks callbacks; final List categories; final int selectedIndex; final ValueChanged onSectionSelected; final int? walletBadge; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Column( key: const Key('home-grid-header'), crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ _searchRow(l10n), _primaryRow(), const SizedBox(height: AppDimens.homeSecondaryGridGap), ], ); } Widget _searchRow(AppLocalizations l10n) { return Padding( padding: const EdgeInsets.fromLTRB( AppDimens.feedPageHorizontal, AppDimens.homeSearchTopGap, AppDimens.feedPageHorizontal, AppDimens.homeSearchPrimaryGap, ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( child: InkWell( key: const Key('home-search-entry'), borderRadius: BorderRadius.circular(AppDimens.searchRadius), onTap: callbacks.onSearch, child: Container( height: AppDimens.searchHeight, padding: const EdgeInsets.symmetric( horizontal: AppDimens.spaceMd, ), decoration: BoxDecoration( color: AppColors.contentSurface, borderRadius: BorderRadius.circular(AppDimens.searchRadius), ), child: Row( children: [ const Icon( Icons.search_rounded, size: AppDimens.homeHeaderIcon, color: AppColors.textSecondary, ), const SizedBox(width: AppDimens.spaceSm), Expanded( child: Text( l10n.homeSearchHint, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.textSecondary, fontSize: AppDimens.searchPlaceholderFont, ), ), ), ], ), ), ), ), const SizedBox(width: AppDimens.spaceSm), _headerAction( key: const Key('home-vip-entry'), frameKey: const Key('home-vip-icon-frame'), imageKey: const Key('home-vip-image'), assetPath: 'assets/icon/crown.png', fallbackIcon: Icons.workspace_premium_outlined, color: AppColors.homeCrownGold, tooltip: l10n.vipMembership, onTap: callbacks.onOpenVip, ), const SizedBox(width: AppDimens.homeActionGap), _headerAction( key: const Key('home-wallet-entry'), frameKey: const Key('home-wallet-icon-frame'), imageKey: const Key('home-wallet-image'), assetPath: 'assets/icon/gift.png', fallbackIcon: Icons.card_giftcard_rounded, color: AppColors.homeGiftPink, tooltip: l10n.wallet, onTap: callbacks.onOpenWallet, badge: walletBadge, ), ], ), ); } Widget _headerAction({ required Key key, required Key frameKey, required Key imageKey, required String assetPath, required IconData fallbackIcon, required Color color, required String tooltip, required VoidCallback onTap, int? badge, }) { return SizedBox.square( dimension: AppDimens.homeActionSlot, child: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: Tooltip( message: tooltip, child: Material( color: AppColors.transparent, child: InkWell( key: key, onTap: onTap, child: Center( child: SizedBox.square( key: frameKey, dimension: AppDimens.homeActionIcon, child: Image.asset( assetPath, key: imageKey, width: AppDimens.homeActionIcon, height: AppDimens.homeActionIcon, fit: BoxFit.contain, errorBuilder: (_, _, _) => Icon( fallbackIcon, size: AppDimens.homeActionIcon, color: color, ), ), ), ), ), ), ), ), if (badge != null && badge > 0) Positioned( key: const Key('home-wallet-badge'), top: 0, right: 0, child: Container( constraints: const BoxConstraints(minWidth: 20, minHeight: 16), padding: const EdgeInsets.symmetric(horizontal: 5), alignment: Alignment.center, decoration: BoxDecoration( color: AppColors.badgeHot, borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Text( formatInteractionCount(badge), style: const TextStyle( color: AppColors.navSelected, fontSize: AppDimens.cardBadgeFont, fontWeight: FontWeight.w700, ), ), ), ), ], ), ); } 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 categories; final int selectedIndex; final ValueChanged onSectionSelected; @override State<_CategoryBar> createState() => _CategoryBarState(); } class _CategoryBarState extends State<_CategoryBar> { final List _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, padding: const EdgeInsets.symmetric( horizontal: AppDimens.feedPageHorizontal, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (var index = 0; index < widget.categories.length; index++) _item(index), ], ), ); } Widget _item(int index) { final category = widget.categories[index]; final selected = widget.selectedIndex == index; return InkWell( 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: AnimatedNavLabel( key: Key('home-primary-label-${category.categoryId}'), label: category.label, selected: selected, alignment: Alignment.bottomLeft, style: const TextStyle( color: AppColors.navSelected, fontSize: AppDimens.homePrimaryTabSelectedFont, letterSpacing: 0, ), ), ), ), ), ); } }