feat(home): 首页按内容类型分频道 Feed 并支持分页

This commit is contained in:
Zhou Weidong committed 2026-10-21 17:33:52 +08:00
1 parent 4d0be47ee3
commit ae61d9ec4c
22 files changed
+1001 -71

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
// Author: fruit.com.cn
/// 首页顶栏的三个内容频道。
enum HomeFeedChannel {
anime('ANIME'),
live('LIVE'),
recommend(null);
const HomeFeedChannel(this.apiType);
final String? apiType;
List<String> get apiTypes => apiType == null ? const [] : <String>[apiType!];
}
+95 -17
View File
@@ -8,7 +8,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../core/config/app_config.dart';
import '../../core/data/models/drama.dart';
import '../../core/data/services/home_feed_controller.dart';
import '../../core/data/services/catalog_controller.dart';
import '../../core/data/services/catalog_retry.dart';
import '../../core/data/services/category_controller.dart';
@@ -24,8 +24,10 @@ import '../discover/discover_types.dart';
import '../discover/discover_view.dart';
import '../create/create_view.dart';
import '../mine/mine_types.dart';
import '../player/widgets/home_feed_header.dart';
import '../player/player_view.dart';
import '../mine/mine_view.dart';
import 'home_feed_channel.dart';
import 'home_binding.dart';
import 'home_logic.dart';
@@ -43,6 +45,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
static const _defaultSplashSize = 384.0;
bool _resumeRetryInFlight = false;
HomeFeedController? _homeFeed;
bool _recommendSeeded = false;
@override
void initState() {
@@ -52,6 +56,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
@override
void dispose() {
_homeFeed?.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -131,11 +136,19 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
);
}
return _HomeTabs(
key: ValueKey(catalog.dramas.first.id),
dramas: catalog.dramas,
onRefreshFeed: catalog.refresh,
);
final homeFeed = _ensureHomeFeed(catalog);
if (!_recommendSeeded) {
_recommendSeeded = true;
homeFeed.seedRecommend(catalog.dramas);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) unawaited(homeFeed.refreshCurrent());
});
}
return _HomeTabs(key: const Key('home-tabs'), feedController: homeFeed);
}
HomeFeedController _ensureHomeFeed(CatalogController catalog) {
return _homeFeed ??= HomeFeedController(catalog.repository);
}
Widget _startupSplash() {
@@ -199,14 +212,9 @@ class HomeFailureView extends StatelessWidget {
/// 四个根页面共享同一路由并保持各自状态。
class _HomeTabs extends StatefulWidget {
const _HomeTabs({
super.key,
required this.dramas,
required this.onRefreshFeed,
});
const _HomeTabs({super.key, required this.feedController});
final List<Drama> dramas;
final Future<bool> Function() onRefreshFeed;
final HomeFeedController feedController;
@override
State<_HomeTabs> createState() => _HomeTabsState();
@@ -218,15 +226,21 @@ class _HomeTabsState extends State<_HomeTabs> {
@override
void initState() {
super.initState();
logic = HomeBinding().create(widget.dramas);
logic = HomeBinding().create(widget.feedController.dramas);
widget.feedController.addListener(_onFeedChanged);
}
@override
void dispose() {
widget.feedController.removeListener(_onFeedChanged);
logic.dispose();
super.dispose();
}
void _onFeedChanged() {
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
@@ -277,16 +291,21 @@ class _HomeTabsState extends State<_HomeTabs> {
}
Widget _home(BuildContext context) {
final dramas = widget.feedController.dramas;
if (dramas.isEmpty) return _feedChannelPlaceholder(context);
return PlayerPage(
homeMode: true,
embedded: true,
active: logic.tab == 0,
drama: widget.dramas.first,
drama: dramas.first,
initialIndex: 0,
feedDramas: widget.dramas,
feedDramas: dramas,
feedChannel: widget.feedController.channel,
onOpenFullPlayer: (drama, episode) =>
logic.playDrama(context, drama, episodeId: episode.id),
onRefreshFeed: widget.onRefreshFeed,
onRefreshFeed: widget.feedController.refreshCurrent,
onLoadMore: widget.feedController.loadMore,
onChannelSelected: _selectFeedChannel,
onDramaChanged: logic.onDramaChanged,
onImmersiveChanged: logic.onImmersiveChanged,
onSearch: () => logic.select(1, search: true),
@@ -295,6 +314,65 @@ class _HomeTabsState extends State<_HomeTabs> {
);
}
Widget _feedChannelPlaceholder(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
fit: StackFit.expand,
children: [
const ColoredBox(color: Colors.black),
SafeArea(
bottom: false,
child: Column(
children: [
HomeFeedHeader(
l10n: l10n,
channel: widget.feedController.channel,
onChannelSelected: _selectFeedChannel,
onRefresh: () => _refreshPlaceholder(context),
onSearch: () => logic.select(1, search: true),
onMore: () {},
),
Expanded(
child: widget.feedController.loadingFirstPage
? const Center(
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator(
key: Key('home-feed-channel-loading'),
strokeWidth: 2.4,
color: Colors.white70,
),
),
)
: EmptyState(
icon: Icons.movie_outlined,
title: l10n.noStories,
actionLabel: l10n.retry,
onAction: () =>
unawaited(widget.feedController.refreshCurrent()),
),
),
],
),
),
],
);
}
Future<bool> _refreshPlaceholder(BuildContext context) async {
final ok = await widget.feedController.refreshCurrent();
if (!ok && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context)!.catalogError)),
);
}
return ok;
}
void _selectFeedChannel(HomeFeedChannel channel) {
unawaited(widget.feedController.selectChannel(channel));
}
Widget _discover(BuildContext context) {
return DiscoverPage(
active: logic.tab == 1,