feat(home): 首页按内容类型分频道 Feed 并支持分页
This commit is contained in:
1 parent
4d0be47ee3
commit
ae61d9ec4c
22 files changed
+1001
-71
No files matched your search
@@ -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!];
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user