// 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/app_bottom_nav.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/server_endpoint_dialog.dart'; import '../../l10n/app_localizations.dart'; import '../discover/discover_view.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, showDebugTools: kDebugMode, ), ); } 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 optional debug-only endpoint controls. class HomeFailureView extends StatelessWidget { const HomeFailureView({ super.key, required this.onRetry, this.showDebugTools = kDebugMode, }); final VoidCallback onRetry; final bool showDebugTools; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Column( children: [ Expanded( child: EmptyState( icon: Icons.cloud_off_rounded, title: l10n.catalogError, actionLabel: l10n.retry, onAction: onRetry, ), ), if (showDebugTools) _debugTools(context, l10n), ], ); } Widget _debugTools(BuildContext context, AppLocalizations l10n) { return Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), child: Column( children: [ Text( AppConfig.apiBaseUrl, style: const TextStyle(fontSize: 12, color: AppColors.muted), ), const SizedBox(height: 8), OutlinedButton.icon( key: const Key('home-server-settings'), onPressed: () => showServerEndpointDialog(context, l10n), icon: const Icon(Icons.dns_outlined), label: Text(l10n.serverSettings), ), ], ), ); } } /// 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; if (!logic.videoImmersive && logic.tab == 1) { unawaited(_openMenu(context)); } else { logic.onSystemBack(); } }, child: Scaffold( key: logic.scaffoldKey, backgroundColor: AppColors.ink, 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: () => _openMenu(context), 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 : AppBottomNav( barKey: const Key('home-tabs-bar'), items: [ AppBottomNavItem( tabKey: const Key('tab-home'), icon: Icons.play_circle_outline_rounded, selectedIcon: Icons.play_circle_fill_rounded, label: l10n.homeTab, selected: logic.tab == 0, onTap: () => _selectTab(context, 0), ), AppBottomNavItem( tabKey: const Key('tab-discover'), icon: Icons.explore_outlined, selectedIcon: Icons.explore_rounded, label: l10n.discoverTab, selected: logic.tab == 1, onTap: () => _selectTab(context, 1), ), AppBottomNavItem( tabKey: const Key('tab-my'), icon: Icons.video_library_outlined, selectedIcon: Icons.video_library_rounded, label: l10n.myTab, selected: logic.tab == 2, onTap: () => _selectTab(context, 2, myProfile: true), ), ], ), ), ); } void _selectTab(BuildContext context, int tab, {bool myProfile = false}) { final same = logic.tab == tab; if (!same) { unawaited(context.read().selectionClick()); } if (tab == 2) { myProfile ? logic.openProfile() : logic.openLibrary(); } else { 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.selectFromMenu(1), onHistory: () => logic.openLibrary(section: 0), onWatchlist: logic.openLibrary, onLiked: () => logic.selectFromMenu(2, mySection: 2), ), ); logic.setMenuOpen(false); } }