Files
ReelShort/lib/pages/home/home_view.dart
T

266 lines
7.6 KiB
Dart

// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../core/config/app_config.dart';
import '../../core/data/services/home_feed_controller.dart';
import '../../core/data/services/catalog_controller.dart';
import '../../core/data/services/category_controller.dart';
import '../../core/data/services/favorite_controller.dart';
import '../../core/data/services/wallet_controller.dart';
import '../../core/platform/haptics.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/app_root_tab.dart';
import '../discover/search_page.dart';
import '../following/following_page.dart';
import '../filter/filter_page.dart';
import '../mine/mine_types.dart';
import '../recommend/recommend_page.dart';
import '../mine/mine_view.dart';
import '../store/vip_page.dart';
import '../wallet/wallet_page.dart';
import 'home_binding.dart';
import 'home_logic.dart';
import 'widgets/home_feed_grid.dart';
import 'widgets/home_grid_header.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
bool _resumeRetryInFlight = false;
HomeFeedController? _homeFeed;
bool _recommendSeeded = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
_homeFeed?.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state != AppLifecycleState.resumed || _resumeRetryInFlight) return;
final catalog = context.read<CatalogController>();
final categories = context.read<CategoryController>();
if (catalog.error == null && categories.error == null) return;
unawaited(_retryAfterResume(catalog, categories));
}
Future<void> _retryAfterResume(
CatalogController catalog,
CategoryController categories,
) async {
_resumeRetryInFlight = true;
try {
await AppConfig.probeAndApplyBaseUrl();
await Future.wait([
if (catalog.error != null) catalog.load(),
if (categories.error != null) categories.load(),
]);
} finally {
_resumeRetryInFlight = false;
}
}
@override
Widget build(BuildContext context) {
final catalog = context.watch<CatalogController>();
return Theme(
data: AppTheme.dark(),
child: _buildHome(context, catalog),
);
}
Widget _buildHome(BuildContext context, CatalogController catalog) {
final homeFeed = _ensureHomeFeed(catalog);
if (!_recommendSeeded && catalog.dramas.isNotEmpty) {
_recommendSeeded = true;
homeFeed.seedRecommend(catalog.dramas);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) unawaited(_refreshRecommendAndPrefetch(homeFeed));
});
}
return _HomeTabs(key: const Key('home-tabs'), feedController: homeFeed);
}
HomeFeedController _ensureHomeFeed(CatalogController catalog) {
return _homeFeed ??= HomeFeedController(catalog.repository);
}
Future<void> _refreshRecommendAndPrefetch(HomeFeedController homeFeed) async {
if (await homeFeed.refreshCurrent()) {
await homeFeed.prefetchChannels();
}
}
}
/// 四个根页面共享同一路由并保持各自状态。
class _HomeTabs extends StatefulWidget {
const _HomeTabs({super.key, required this.feedController});
final HomeFeedController feedController;
@override
State<_HomeTabs> createState() => _HomeTabsState();
}
class _HomeTabsState extends State<_HomeTabs> {
late final HomeLogic logic;
@override
void initState() {
super.initState();
logic = HomeBinding().create(widget.feedController.dramas);
}
@override
void dispose() {
logic.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: logic,
builder: (context, _) => _scaffold(context),
);
}
Widget _scaffold(BuildContext context) {
final mediaTab = logic.tab == 0 || logic.tab == 1;
final content = PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (didPop) return;
logic.onSystemBack();
},
child: Scaffold(
key: logic.scaffoldKey,
backgroundColor: AppColors.transparent,
resizeToAvoidBottomInset: false,
body: _body(context),
bottomNavigationBar: logic.videoImmersive ? null : _nav(context),
),
);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light,
child: mediaTab
? content
: DecoratedBox(
key: const Key('home-page-gradient'),
decoration: const BoxDecoration(gradient: AppTheme.pageGradient),
child: content,
),
);
}
Widget _body(BuildContext context) {
return IndexedStack(
index: logic.tab,
children: [
_homeGrid(context),
_recommend(context),
FollowingPage(
onGoHome: () => logic.select(0),
favoriteController: context.read<FavoriteController>(),
historyRequest: logic.followingHistoryRequest,
),
_my(context),
],
);
}
Widget _homeGrid(BuildContext context) {
return HomeFeedGrid(
feedController: widget.feedController,
callbacks: HomeGridCallbacks(
onSearch: () => _openSearch(context),
onOpenVip: () => unawaited(_openVip(context)),
onOpenWallet: () => unawaited(_openWallet(context)),
onOpenFilter: () => _pushPage(context, const FilterPage()),
),
onOpenDrama: (drama) => unawaited(logic.playDrama(context, drama)),
);
}
Widget _recommend(BuildContext context) {
return RecommendPage(
feedController: widget.feedController,
active: logic.tab == 1,
onSearch: () => _openSearch(context),
);
}
void _openSearch(BuildContext context) {
Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => const SearchPage()));
}
void _pushPage(BuildContext context, Widget page) {
Navigator.of(context).push<void>(MaterialPageRoute(builder: (_) => page));
}
Future<void> _openVip(BuildContext context) async {
if (!await ensureSignedIn(context) || !context.mounted) return;
final wallet = context.read<WalletController>();
await Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (_) => VipPage(onPurchased: () => unawaited(wallet.load())),
),
);
}
Future<void> _openWallet(BuildContext context) async {
if (!await ensureSignedIn(context) || !context.mounted) return;
await Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => const WalletPage()));
}
Widget _my(BuildContext context) {
return MyPage(
active: logic.tab == 3,
callbacks: MyPageCallbacks(
onAction: logic.myAction,
onOpenHistoryTab: logic.openFollowingHistory,
),
);
}
Widget _nav(BuildContext context) {
return AppRootBottomNav(
barKey: const Key('home-tabs-bar'),
selected: AppRootTab.values[logic.tab],
onSelected: (tab) => _selectTab(context, tab.index),
);
}
void _selectTab(BuildContext context, int tab) {
final same = logic.tab == tab;
if (!same) {
unawaited(context.read<Haptics>().selectionClick());
}
logic.select(tab);
}
}