// 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/category_controller.dart'; import '../../core/data/services/home_feed_controller.dart'; import '../../core/theme/app_colors.dart'; import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; import '../player/open_player.dart'; import '../player/player_view.dart'; import 'recommend_types.dart'; import 'widgets/all_dramas_filter_grid.dart'; import 'widgets/recommend_sections_view.dart'; import 'widgets/recommend_top_nav.dart'; class RecommendPage extends StatefulWidget { const RecommendPage({ super.key, required this.feedController, required this.active, required this.onSearch, }); final HomeFeedController feedController; final bool active; final VoidCallback onSearch; @override State createState() => _RecommendPageState(); } class _RecommendPageState extends State { RecommendInnerTab _tab = RecommendInnerTab.feed; final PageController _pageController = PageController(); int _scrubCancelRequest = 0; @override void dispose() { _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final categories = context.read(); return ColoredBox( key: const Key('recommend-page'), color: AppColors.appBackground, child: Stack( children: [ Positioned.fill( child: ListenableBuilder( listenable: Listenable.merge([widget.feedController, categories]), builder: (context, _) => PageView( key: const Key('recommend-tab-page-view'), controller: _pageController, scrollDirection: Axis.horizontal, onPageChanged: _onPageChanged, children: [ _KeepAliveTab( key: const PageStorageKey('recommend-tab-feed'), child: _recommendFeed(context), ), _KeepAliveTab( key: const PageStorageKey('recommend-tab-playlists'), child: _recommendPlaylists(categories), ), _KeepAliveTab( key: const PageStorageKey('recommend-tab-all'), child: _withTopNavSpace( AllDramasFilterGrid( feedController: widget.feedController, onOpenDrama: _openDrama, ), ), ), ], ), ), ), Positioned( top: 0, left: 0, right: 0, child: IgnorePointer( child: SizedBox( height: 72, child: DecoratedBox( key: const Key('recommend-top-gradient'), decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [AppColors.mediaBlack, AppColors.transparent], ), ), ), ), ), ), Positioned( top: 0, left: 0, right: 0, child: SafeArea( bottom: false, child: RecommendTopNav( selected: _tab, onVideo: _tab == RecommendInnerTab.feed, onSelected: _selectTab, onSearch: _handleSearch, ), ), ), ], ), ); } Widget _recommendFeed(BuildContext context) { final dramas = widget.feedController.dramas; if (dramas.isEmpty) { final l10n = AppLocalizations.of(context)!; if (widget.feedController.loadingFirstPage) { return const _RecommendFeedSkeleton(); } return ColoredBox( color: AppColors.mediaBlack, child: EmptyState( key: const Key('recommend-feed-empty'), icon: Icons.movie_outlined, title: l10n.noStories, actionLabel: l10n.retry, onAction: () => unawaited(widget.feedController.refreshCurrent()), ), ); } return PlayerPage( key: const Key('recommend-feed-player'), homeMode: true, embedded: true, active: widget.active && _tab == RecommendInnerTab.feed, showHomeFeedHeader: false, drama: dramas.first, initialIndex: 0, feedDramas: dramas, feedChannel: widget.feedController.channel, onOpenFullPlayer: (drama, episode) => _openDrama(drama, episodeId: episode.id), onRefreshFeed: widget.feedController.refreshCurrent, onLoadMore: widget.feedController.loadMore, scrubCancelRequest: _scrubCancelRequest, ); } Widget _recommendPlaylists(CategoryController categories) { return DecoratedBox( key: const Key('recommend-playlist-surface'), decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColors.mineGradientTop, AppColors.mineGradientMiddle, AppColors.mineGradientBottom, ], stops: [0, 0.4, 0.75], ), ), child: Stack( fit: StackFit.expand, children: [ const DecoratedBox( key: Key('recommend-playlist-glow'), decoration: BoxDecoration( gradient: RadialGradient( center: Alignment(0, -0.95), radius: 0.9, colors: [ AppColors.recommendPlaylistGlow, AppColors.transparent, ], ), ), ), _withTopNavSpace( RecommendSectionsView( feedController: widget.feedController, categories: categories, onOpenDrama: _openDrama, onMore: _handleMore, ), ), ], ), ); } void _selectTab(RecommendInnerTab tab) { setState(() { _tab = tab; _scrubCancelRequest++; }); if (_pageController.hasClients) { unawaited( _pageController.animateToPage( tab.index, duration: const Duration(milliseconds: 220), curve: Curves.easeOut, ), ); } } void _onPageChanged(int index) { final next = RecommendInnerTab.values[index]; if (next == _tab) return; setState(() { _tab = next; _scrubCancelRequest++; }); } void _handleSearch() { setState(() => _scrubCancelRequest++); widget.onSearch(); } Widget _withTopNavSpace(Widget child) { return SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.only(top: RecommendTopNav.height), child: child, ), ); } void _handleMore(RecommendSectionKind section) { _selectTab(RecommendInnerTab.allDramas); } Future _openDrama(Drama drama, {String? episodeId}) async { await playDramaFromCatalog(context, drama, episodeId: episodeId); } } class _RecommendFeedSkeleton extends StatelessWidget { const _RecommendFeedSkeleton(); @override Widget build(BuildContext context) { return const ColoredBox( key: Key('recommend-feed-skeleton'), color: AppColors.mediaBlack, child: Stack( children: [ Center( child: SizedBox( width: 132, height: 176, child: DecoratedBox( decoration: BoxDecoration( color: AppColors.skeletonSurface, borderRadius: BorderRadius.all(Radius.circular(12)), ), ), ), ), Positioned( left: 14, right: 72, bottom: 42, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SkeletonLine(width: 190, height: 18), SizedBox(height: 10), _SkeletonLine(width: 140, height: 12), SizedBox(height: 8), _SkeletonLine(width: 220, height: 10), ], ), ), ], ), ); } } class _SkeletonLine extends StatelessWidget { const _SkeletonLine({required this.width, required this.height}); final double width; final double height; @override Widget build(BuildContext context) { return Container( width: width, height: height, decoration: BoxDecoration( color: AppColors.skeletonSurface, borderRadius: BorderRadius.circular(6), ), ); } } class _KeepAliveTab extends StatefulWidget { const _KeepAliveTab({super.key, required this.child}); final Widget child; @override State<_KeepAliveTab> createState() => _KeepAliveTabState(); } class _KeepAliveTabState extends State<_KeepAliveTab> with AutomaticKeepAliveClientMixin<_KeepAliveTab> { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return widget.child; } }