Files
ReelShort/lib/pages/recommend/recommend_page.dart
T

244 lines
7.4 KiB
Dart

// 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/category_controller.dart';
import '../../core/data/services/download_controller.dart';
import '../../core/data/services/home_feed_controller.dart';
import '../../core/data/services/wallet_controller.dart';
import '../../core/navigation/fade_page_route.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart';
import '../../core/widgets/empty_state.dart';
import '../../l10n/app_localizations.dart';
import '../player/player_view.dart';
import 'widgets/all_dramas_filter_grid.dart';
import 'widgets/recommend_sections_view.dart';
enum RecommendInnerTab { feed, playlists, allDramas }
class RecommendPage extends StatefulWidget {
const RecommendPage({
super.key,
required this.feedController,
required this.active,
required this.onSearch,
required this.onOpenFilter,
});
final HomeFeedController feedController;
final bool active;
final VoidCallback onSearch;
final VoidCallback onOpenFilter;
@override
State<RecommendPage> createState() => _RecommendPageState();
}
class _RecommendPageState extends State<RecommendPage> {
RecommendInnerTab _tab = RecommendInnerTab.feed;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final categories = context.watch<CategoryController>();
return ColoredBox(
key: const Key('recommend-page'),
color: AppColors.appBackground,
child: SafeArea(
bottom: false,
child: Column(
children: [
_header(l10n),
Expanded(
child: IndexedStack(
index: _tab.index,
children: [
_recommendFeed(context),
RecommendSectionsView(
feedController: widget.feedController,
categories: categories,
onOpenDrama: _openDrama,
onMore: _handleMore,
),
AllDramasFilterGrid(
feedController: widget.feedController,
onOpenDrama: _openDrama,
onOpenFilter: widget.onOpenFilter,
),
],
),
),
],
),
),
);
}
Widget _recommendFeed(BuildContext context) {
final dramas = widget.feedController.dramas;
if (dramas.isEmpty) {
final l10n = AppLocalizations.of(context)!;
if (widget.feedController.loadingFirstPage) {
return const ColoredBox(
color: AppColors.mediaBlack,
child: Center(
child: CircularProgressIndicator(
key: Key('recommend-feed-loading'),
color: AppColors.muted,
),
),
);
}
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,
);
}
Widget _header(AppLocalizations l10n) {
final items = [
(RecommendInnerTab.feed, l10n.recommendTopTab, 'recommend'),
(RecommendInnerTab.playlists, l10n.playlistTopTab, 'playlist'),
(RecommendInnerTab.allDramas, l10n.allDramasTopTab, 'all'),
];
return SizedBox(
key: const Key('recommend-top-nav'),
height: 56,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
),
child: Row(
children: [
Expanded(
child: Row(
children: [
for (final item in items)
Expanded(
child: _tabButton(
key: Key('recommend-tab-${item.$3}'),
label: item.$2,
selected: _tab == item.$1,
onTap: () => setState(() => _tab = item.$1),
),
),
],
),
),
const SizedBox(width: AppDimens.spaceSm),
IconButton(
key: const Key('recommend-search'),
tooltip: l10n.search,
onPressed: widget.onSearch,
iconSize: 24,
color: AppColors.textPrimary,
icon: const Icon(Icons.search_rounded),
),
],
),
),
);
}
Widget _tabButton({
required Key key,
required String label,
required bool selected,
required VoidCallback onTap,
}) {
return InkWell(
key: key,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.only(right: AppDimens.spaceSm),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: selected
? AppColors.textPrimary
: AppColors.textTertiary,
fontSize: AppDimens.homePrimaryTabFont,
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
),
),
const SizedBox(height: 3),
Container(
width: selected ? 20 : 0,
height: 2,
decoration: BoxDecoration(
color: AppColors.textPrimary,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
),
],
),
),
);
}
void _handleMore(RecommendSectionKind section) {
setState(() => _tab = RecommendInnerTab.allDramas);
}
Future<void> _openDrama(Drama drama, {String? episodeId}) async {
final catalog = context.read<CatalogController>();
final wallet = context.read<WalletController>();
final downloads = context.read<DownloadController>();
var playable = drama;
try {
playable = await catalog.requireDetail(drama);
} catch (_) {
// 卡片预览仍可直接进入播放器。
}
if (!mounted) return;
final episodeIndex = episodeId == null
? 0
: playable.episodes.indexWhere((episode) => episode.id == episodeId);
await Navigator.of(context).push<void>(
FadePageRoute(
builder: (_) => PlayerPage(
drama: playable,
initialIndex: episodeIndex < 0 ? 0 : episodeIndex,
homeMode: true,
feedDramas: [playable],
wallet: wallet,
downloads: downloads,
),
),
);
}
}