// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/config/app_config.dart'; import '../../core/data/models/drama.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/data/services/catalog_controller.dart'; import '../../core/platform/haptics.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../l10n/app_localizations.dart'; import '../discover/discover_view.dart'; import '../mine/mine_view.dart'; import '../player/player_view.dart'; import 'home_binding.dart'; import 'home_logic.dart'; import 'widgets/home_quick_drawer.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { final catalog = context.watch(); 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: Colors.black, body: Center(child: CircularProgressIndicator()), ); } if (catalog.error != null || catalog.dramas.isEmpty) { return Scaffold( backgroundColor: Colors.black, body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(catalog.error == null ? l10n.noStories : l10n.catalogError), const SizedBox(height: 12), FilledButton(onPressed: catalog.load, child: Text(l10n.retry)), if (kDebugMode) ...[ const SizedBox(height: 16), Text( AppConfig.apiBaseUrl, style: const TextStyle(fontSize: 12, color: Colors.white54), ), ], ], ), ), ); } return _HomeTabs( key: ValueKey(catalog.dramas.first.id), dramas: catalog.dramas, ); } } /// 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)!; // 登录态统一从 AuthController 取(原来是 home_logic 里的本地演示开关) final signedIn = context.watch().isSignedIn; return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (didPop) return; logic.onSystemBack(); }, child: Scaffold( key: logic.scaffoldKey, backgroundColor: AppColors.ink, drawer: HomeQuickDrawer( drama: logic.currentDrama, onOpenSeries: () => logic.openCurrentSeries(context), onDiscover: () => logic.selectFromMenu(1), onHistory: () => logic.openLibrary(section: 0), onWatchlist: logic.openLibrary, onLiked: () => logic.selectFromMenu(2, mySection: 2), ), drawerEnableOpenDragGesture: false, onDrawerChanged: logic.onDrawerChanged, body: IndexedStack( index: logic.tab, children: [ 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: logic.openMenu, onImmersiveChanged: logic.onImmersiveChanged, ), DiscoverPage( drama: logic.currentDrama, dramas: widget.dramas, onDramaSelected: (drama) => logic.playDrama(context, drama), duration: Duration.zero, embedded: true, active: logic.tab == 1, searchRequest: logic.searchRequest, signedIn: signedIn, onSignIn: () => openAuthPage(context), onCoins: logic.openProfile, onRewards: logic.openProfile, onAction: (action) => logic.discoverAction(context, action), ), MyPage( drama: logic.currentDrama, dramas: widget.dramas, onDramaSelected: (drama) => logic.openDrama(context, drama), duration: Duration.zero, initialSection: logic.mySection, profile: logic.profile, sectionRequest: logic.sectionRequest, embedded: true, active: logic.tab == 2, onAction: logic.myAction, signedIn: signedIn, onSignIn: () => openAuthPage(context), onSignOut: () => context.read().signOut(), ), ], ), bottomNavigationBar: logic.videoImmersive ? null : SafeArea( top: false, minimum: const EdgeInsets.only(bottom: 2), child: Container( key: const Key('home-tabs-bar'), height: 64, decoration: BoxDecoration( color: AppColors.bar, border: Border( top: BorderSide( color: Theme.of(context).colorScheme.outlineVariant, ), ), ), child: Row( children: [ _TabButton( key: const Key('tab-discover'), icon: Icons.home_outlined, selectedIcon: Icons.home_rounded, label: l10n.homeTab, selected: logic.tab == 1, onTap: () => _selectTab(context, 1), ), _TabButton( key: const Key('tab-home'), icon: Icons.movie_outlined, selectedIcon: Icons.movie_rounded, label: l10n.shortsTab, selected: logic.tab == 0, onTap: () => _selectTab(context, 0), ), _TabButton( key: const Key('tab-my-list'), icon: Icons.bookmark_border_rounded, selectedIcon: Icons.bookmark_rounded, label: l10n.myListTab, selected: logic.tab == 2 && !logic.profile, onTap: () => _selectTab(context, 2), ), _TabButton( key: const Key('tab-my'), icon: Icons.person_outline_rounded, selectedIcon: Icons.person_rounded, label: l10n.myTab, selected: logic.tab == 2 && logic.profile, onTap: () => _selectTab(context, 2, myProfile: true), ), ], ), ), ), ), ); } void _selectTab(BuildContext context, int tab, {bool myProfile = false}) { final same = tab == 2 ? logic.tab == 2 && logic.profile == myProfile : logic.tab == tab; if (!same) { unawaited(context.read().selectionClick()); } if (tab == 2) { myProfile ? logic.openProfile() : logic.openLibrary(); } else { logic.select(tab); } } } class _TabButton extends StatelessWidget { const _TabButton({ super.key, required this.icon, required this.selectedIcon, required this.label, required this.selected, required this.onTap, }); final IconData icon; final IconData selectedIcon; final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) => Expanded( child: Semantics( button: true, selected: selected, label: label, child: InkWell( onTap: onTap, child: ConstrainedBox( constraints: const BoxConstraints(minHeight: 48), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AnimatedContainer( duration: const Duration(milliseconds: 160), curve: Curves.easeOut, padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 3, ), decoration: BoxDecoration( color: selected ? AppColors.bluePressed : null, borderRadius: BorderRadius.circular(14), ), child: Icon( selected ? selectedIcon : icon, size: 22, color: selected ? AppColors.onPrimary : AppColors.muted, ), ), const SizedBox(height: 2), Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 10, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, color: selected ? AppColors.blueSoft : AppColors.muted, ), ), ], ), ), ), ), ); }