// 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/catalog_controller.dart'; import '../../core/data/services/download_controller.dart'; import '../../core/data/services/wallet_controller.dart'; import '../../core/platform/haptics.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; import '../discover/discover_types.dart'; import '../discover/discover_view.dart'; import '../mine/mine_types.dart'; import '../splash/splash_page.dart'; import '../mine/mine_view.dart'; import '../player/player_view.dart'; import 'home_binding.dart'; import 'home_logic.dart'; import 'widgets/home_menu_sheet.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { final catalog = context.watch(); if (!catalog.settled) return const SplashPage(); final l10n = AppLocalizations.of(context)!; return Theme( data: AppTheme.dark(), child: _buildHome(context, catalog, l10n), ); } Widget _buildHome( BuildContext context, CatalogController catalog, AppLocalizations l10n, ) { if (catalog.loading) { return const Scaffold( backgroundColor: AppColors.ink, body: Center(child: CircularProgressIndicator()), ); } if (catalog.error != null) { return Scaffold( backgroundColor: AppColors.ink, body: HomeFailureView(onRetry: catalog.load), ); } if (catalog.dramas.isEmpty) { return Scaffold( backgroundColor: AppColors.ink, body: EmptyState( key: const Key('home-empty'), icon: Icons.movie_outlined, title: l10n.noStories, subtitle: l10n.emptyHint, actionLabel: l10n.retry, onAction: catalog.load, ), ); } return _HomeTabs( key: ValueKey(catalog.dramas.first.id), dramas: catalog.dramas, ); } } /// Home catalog failure with a single recovery action. class HomeFailureView extends StatelessWidget { const HomeFailureView({super.key, required this.onRetry}); final VoidCallback onRetry; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return EmptyState( icon: Icons.cloud_off_rounded, title: l10n.catalogError, actionLabel: l10n.retry, onAction: onRetry, ); } } /// The three primary destinations share a single route and keep their state. class _HomeTabs extends StatefulWidget { const _HomeTabs({super.key, required this.dramas}); final List dramas; @override State<_HomeTabs> createState() => _HomeTabsState(); } class _HomeTabsState extends State<_HomeTabs> { late final HomeLogic logic; @override void initState() { super.initState(); logic = HomeBinding().create(widget.dramas); } @override void dispose() { logic.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: logic, builder: (context, _) => _scaffold(context), ); } Widget _scaffold(BuildContext context) { final l10n = AppLocalizations.of(context)!; return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (didPop) return; if (!logic.videoImmersive && logic.tab == 1) { unawaited(_openMenu(context)); } else { logic.onSystemBack(); } }, child: Scaffold( key: logic.scaffoldKey, backgroundColor: AppColors.ink, body: _body(context), bottomNavigationBar: logic.videoImmersive ? null : _nav(context, l10n), ), ); } Widget _body(BuildContext context) { return IndexedStack( index: logic.tab, children: [_player(context), _discover(context), _my(context)], ); } Widget _player(BuildContext context) { return PlayerPage( drama: widget.dramas.first, feedDramas: widget.dramas, onDramaChanged: logic.onDramaChanged, initialIndex: 0, homeMode: true, embedded: true, active: logic.tab == 0 && !logic.drawerOpen && !logic.detailOpen, resumeRequest: logic.resumeRequest, backRequest: logic.backRequest, onDiscover: () => logic.select(1), onMy: logic.openProfile, onSearch: () => logic.select(1, search: true), onMenu: () => _openMenu(context), onImmersiveChanged: logic.onImmersiveChanged, wallet: context.read(), downloads: context.read(), ); } Widget _discover(BuildContext context) { return DiscoverPage( active: logic.tab == 1, searchRequest: logic.searchRequest, callbacks: DiscoverPageCallbacks( onAction: logic.discoverAction, onDramaSelected: (drama) => logic.playDrama(context, drama), onProfile: logic.openProfile, ), ); } Widget _my(BuildContext context) { return MyPage( active: logic.tab == 2, callbacks: MyPageCallbacks( onAction: logic.myAction, onDramaSelected: (drama) => logic.openDrama(context, drama), ), ); } Widget _nav(BuildContext context, AppLocalizations l10n) { return AppBottomNav( barKey: const Key('home-tabs-bar'), items: [ _navItem(context, l10n, 0), _navItem(context, l10n, 1), _navItem(context, l10n, 2), ], ); } AppBottomNavItem _navItem( BuildContext context, AppLocalizations l10n, int index, ) { final selected = logic.tab == index; return switch (index) { 0 => AppBottomNavItem( tabKey: const Key('tab-home'), icon: Icons.play_circle_outline_rounded, selectedIcon: Icons.play_circle_fill_rounded, label: l10n.homeTab, selected: selected, onTap: () => _selectTab(context, index), ), 1 => AppBottomNavItem( tabKey: const Key('tab-discover'), icon: Icons.explore_outlined, selectedIcon: Icons.explore_rounded, label: l10n.discoverTab, selected: selected, onTap: () => _selectTab(context, index), ), _ => AppBottomNavItem( tabKey: const Key('tab-my'), icon: Icons.video_library_outlined, selectedIcon: Icons.video_library_rounded, label: l10n.myTab, selected: selected, onTap: () => _selectTab(context, index), ), }; } void _selectTab(BuildContext context, int tab) { final same = logic.tab == tab; if (!same) { unawaited(context.read().selectionClick()); } logic.select(tab); } Future _openMenu(BuildContext context) async { logic.setMenuOpen(true); await showModalBottomSheet( context: context, backgroundColor: AppColors.card, showDragHandle: true, isScrollControlled: true, builder: (_) => HomeMenuSheet( drama: logic.currentDrama, onOpenSeries: () => logic.openCurrentSeries(context), onDiscover: () => logic.select(1), onHistory: () => logic.openHistory(context), onWatchlist: () => logic.openFavorites(context), ), ); logic.setMenuOpen(false); } }