// Author: fruit.com.cn import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../core/data/models/drama.dart'; import '../../core/data/repositories/search_repository.dart'; import '../../core/data/services/category_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.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 '../drama_detail/drama_detail_view.dart'; import 'discover_binding.dart'; import 'discover_logic.dart'; import 'discover_types.dart'; import 'widgets/discover_header.dart'; import 'widgets/discover_search_results.dart'; import 'widgets/drama_grid.dart'; /// K28-A2 剧场页:搜索、分类/语言 chips 与双列剧库。 class DiscoverPage extends StatefulWidget { const DiscoverPage({ super.key, this.active = true, this.searchRequest = 0, this.focusSearch = false, this.callbacks = const DiscoverPageCallbacks(), }); final bool active; final int searchRequest; final bool focusSearch; final DiscoverPageCallbacks callbacks; @override State createState() => _DiscoverPageState(); } class _DiscoverPageState extends State { late final DiscoverLogic logic; bool get embedded => widget.callbacks.embedded; @override void initState() { super.initState(); logic = DiscoverBinding().create(search: context.read()); if (widget.focusSearch) _focusOnFirstFrame(); } void _focusOnFirstFrame() { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && widget.active) logic.openSearch(); }); } @override void didUpdateWidget(covariant DiscoverPage oldWidget) { super.didUpdateWidget(oldWidget); logic.syncSearch( active: widget.active, request: widget.searchRequest, previousRequest: oldWidget.searchRequest, isActive: () => mounted && widget.active, ); } @override void dispose() { logic.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: logic, builder: (context, _) => _page(context), ); } Widget _page(BuildContext context) { final l10n = AppLocalizations.of(context)!; final categories = context.watch(); final content = SafeArea( bottom: !embedded, child: Column( children: [ DiscoverHeader( controller: logic.search, focusNode: logic.searchFocus, onChanged: (_) => logic.onSearchChanged(), loading: logic.searchLoading, onProfile: _openProfile, ), Expanded(child: _feed(l10n, categories)), if (!embedded) _bottomNav(l10n), ], ), ); return AnnotatedRegion( value: SystemUiOverlayStyle.dark, child: Theme( data: AppTheme.light(), child: embedded ? content : Scaffold(backgroundColor: AppColors.ink, body: content), ), ); } Widget _feed(AppLocalizations l10n, CategoryController categories) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: logic.searchFocus.unfocus, child: RefreshIndicator( key: const Key('discover-refresh'), color: AppColors.blue, backgroundColor: AppColors.card, onRefresh: categories.load, child: ListView( controller: logic.scroll, physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only(bottom: AppDimens.spaceXl), children: _content(l10n, categories), ), ), ); } List _content(AppLocalizations l10n, CategoryController categories) { if (logic.hasSearchQuery) { return [ DiscoverSearchResults( loading: logic.searchLoading, error: logic.searchError, results: logic.searchResults, onRetry: logic.retrySearch, onOpen: _openDrama, ), ]; } if (categories.loading && categories.feed.sections.isEmpty) { return [_block(const CircularProgressIndicator(strokeWidth: 2))]; } final error = categories.error; if (error != null) { return [ _block( EmptyState( key: const Key('home-categories-error'), icon: Icons.cloud_off_rounded, title: errorText(l10n, error, l10n.categoriesError), subtitle: l10n.emptyHint, actionLabel: l10n.retry, onAction: categories.load, ), ), ]; } return _theaterBlocks(l10n, categories); } List _theaterBlocks( AppLocalizations l10n, CategoryController categories, ) { final dramas = logic.selectedCategoryId == null ? categories.feed.dramas : categories.dramasOf(logic.selectedCategoryId!); return [ _categoryChips(l10n, categories), if (dramas.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 40), child: EmptyState( key: const Key('discover-empty'), icon: Icons.movie_filter_outlined, title: l10n.noStories, subtitle: l10n.emptyHint, ), ) else DramaGrid(l10n: l10n, dramas: dramas, onOpen: _openDrama), ]; } Widget _categoryChips(AppLocalizations l10n, CategoryController categories) { return Padding( padding: const EdgeInsets.fromLTRB( AppDimens.pageHorizontal, AppDimens.spaceMd, AppDimens.pageHorizontal, 0, ), child: Wrap( key: const Key('theater-category-chips'), spacing: 10, runSpacing: 10, children: [ _filterChip( key: const Key('theater-category-all'), label: l10n.all, selected: logic.selectedCategoryId == null, onSelected: () => logic.selectCategory(null), ), for (final category in categories.categories) _filterChip( key: Key('theater-category-${category.id}'), label: category.name, selected: logic.selectedCategoryId == category.id, onSelected: () => logic.selectCategory(category.id), ), ], ), ); } Widget _filterChip({ required Key key, required String label, required bool selected, required VoidCallback onSelected, }) { return ChoiceChip( key: key, label: Text(label), selected: selected, showCheckmark: false, onSelected: (_) => onSelected(), backgroundColor: AppColors.card, selectedColor: AppColors.blue, side: BorderSide(color: selected ? AppColors.blue : AppColors.border), labelStyle: TextStyle( color: selected ? AppColors.onPrimary : AppColors.onSurface, fontSize: AppDimens.caption, fontWeight: FontWeight.w700, ), shape: const StadiumBorder(), ); } Widget _block(Widget child) => SizedBox(height: 280, child: Center(child: child)); void _openProfile() { final callback = widget.callbacks.onProfile; if (callback != null) { callback(); return; } _act(DiscoverPageAction.my); } void _act(DiscoverPageAction action) { logic.act( context, action, embedded: embedded, onAction: widget.callbacks.onAction, ); } void _openDrama(Drama drama) { final callback = widget.callbacks.onDramaSelected; if (callback != null) { callback(drama); return; } Navigator.of(context).push( MaterialPageRoute(builder: (_) => DramaDetailPage(drama: drama)), ); } Widget _bottomNav(AppLocalizations l10n) => AppBottomNav( items: [ AppBottomNavItem( tabKey: const Key('discover-nav-home'), icon: Icons.home_outlined, selectedIcon: Icons.home_rounded, label: l10n.homeTab, onTap: () => _act(DiscoverPageAction.home), ), AppBottomNavItem( tabKey: const Key('discover-nav-current'), icon: Icons.explore_outlined, selectedIcon: Icons.explore_rounded, label: l10n.theaterTab, selected: true, onTap: () {}, ), AppBottomNavItem( tabKey: const Key('discover-nav-create'), icon: Icons.add_box_outlined, selectedIcon: Icons.add_box_rounded, label: l10n.createTab, onTap: () => _act(DiscoverPageAction.create), ), AppBottomNavItem( tabKey: const Key('discover-nav-my'), icon: Icons.person_outline_rounded, selectedIcon: Icons.person_rounded, label: l10n.myTab, onTap: () => _act(DiscoverPageAction.my), ), ], ); }