Files
ReelShort/lib/pages/home/widgets/home_grid_header.dart
T

344 lines
10 KiB
Dart

// 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,
});
final String label;
final String categoryName;
final String categoryId;
final HomePrimaryTab tab;
}
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<HomeGridCategoryShortcut> categories;
final int selectedIndex;
final ValueChanged<int> 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<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,
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,
),
),
),
),
),
);
}
}