247 lines
7.5 KiB
Dart
247 lines
7.5 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.read<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: [
|
|
ListenableBuilder(
|
|
listenable: widget.feedController,
|
|
builder: (context, _) => _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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|