import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/main.dart' show routeObserver; import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/modules/home/index_home_page.dart'; import 'package:yumi/modules/chat/message/sc_message_page.dart'; import 'package:yumi/modules/user/task/task_page.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; import '../../shared/tools/sc_heartbeat_utils.dart'; import '../../shared/tools/sc_entry_popup_coordinator.dart'; import '../../shared/tools/sc_lk_event_bus.dart'; import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; import '../../ui_kit/components/sc_float_ichart.dart'; import '../../ui_kit/widgets/daily_sign_in/daily_sign_in_dialog.dart'; import '../../ui_kit/widgets/register_reward/register_reward_dialog.dart'; import '../user/me_page2.dart'; /// 首页 class SCIndexPage extends StatefulWidget { const SCIndexPage({super.key}); @override State createState() => _SCIndexPageState(); } class _SCIndexPageState extends State with WidgetsBindingObserver, RouteAware { static const Duration _registerRewardSocketWaitTimeout = Duration(seconds: 6); int _currentIndex = 0; final List _pages = []; final Set _builtPageIndexes = {0}; final List _bottomItems = []; final ValueNotifier _taskClaimableCount = ValueNotifier(0); SCAppGeneralManager? generalProvider; Locale? _lastLocale; bool _hasShownEntryDialogs = false; bool _hasShownRegisterRewardDialog = false; bool _isShowingDailySignInDialog = false; bool _showRegisterRewardAfterDailySignIn = false; bool _isOpeningFirstRegisterRoomGame = false; StreamSubscription? _registerRewardSubscription; Completer? _registerRewardSocketCompleter; PageRoute? _routeObserverRoute; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _registerRewardSubscription = eventBus .on() .listen(_onRegisterRewardGranted); _initializePages(); unawaited(_loadTaskClaimableCount()); generalProvider = Provider.of(context, listen: false); final rtcProvider = Provider.of(context, listen: false); rtcProvider.initializeRealTimeCommunicationManager(context); unawaited( rtcProvider.prewarmRtcEngine().catchError((error, stackTrace) {}), ); Provider.of(context, listen: false).init(context); Provider.of( context, listen: false, ).getUserIdentity(); Provider.of(context, listen: false).balance(); SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); DataPersistence.setLang(SCGlobalConfig.lang); OverlayManager().activate(); WidgetsBinding.instance.addPostFrameCallback((_) { WakelockPlus.enable(); _showEntryDialogs(); }); unawaited(rtcProvider.cleanupPersistedVoiceRoomSessionBeforeEntry()); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); routeObserver.unsubscribe(this); OverlayManager().setHomeRootTabsVisible(false); _registerRewardSubscription?.cancel(); _taskClaimableCount.dispose(); WakelockPlus.disable(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state != AppLifecycleState.resumed) { return; } unawaited( Provider.of( context, listen: false, ).syncRoomRedPacketBroadcastGroup(), ); WidgetsBinding.instance.addPostFrameCallback((_) { unawaited(_showEntryPopupIfNeeded()); }); unawaited(_loadTaskClaimableCount()); } @override void didChangeDependencies() { super.didChangeDependencies(); SCFloatIchart().init(context); _rebuildBottomItemsIfNeeded(); final route = ModalRoute.of(context); if (route is PageRoute && _routeObserverRoute != route) { if (_routeObserverRoute != null) { routeObserver.unsubscribe(this); } _routeObserverRoute = route; routeObserver.subscribe(this, route); _setHomeRootTabsVisible(route.isCurrent); } } @override void didPush() { _setHomeRootTabsVisible(true); } @override void didPopNext() { _setHomeRootTabsVisible(true); unawaited(_loadTaskClaimableCount()); } @override void didPushNext() { _setHomeRootTabsVisible(false); } @override void didPop() { _setHomeRootTabsVisible(false); } void _setHomeRootTabsVisible(bool visible) { if (!mounted && visible) { return; } OverlayManager().setHomeRootTabsVisible(visible); if (visible) { unawaited( Provider.of( context, listen: false, ).syncRoomRedPacketBroadcastGroup(), ); } } @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) async { if (didPop) { return; } final shouldPop = await _doubleExit(); if (shouldPop && context.mounted) { Navigator.of(context).pop(result); } }, child: Stack( children: [ Image.asset( "sc_images/index/sc_icon_index_bg.png", width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, fit: BoxFit.fill, ), SafeArea( top: false, child: Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, body: _buildCachedBody(), bottomNavigationBar: Container( height: 85.w, decoration: BoxDecoration( image: DecorationImage( image: AssetImage( "sc_images/index/sc_index_bottom_navigation_bar_bg.png", ), fit: BoxFit.fill, ), ), child: Theme( data: Theme.of(context).copyWith( splashFactory: NoSplash.splashFactory, splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, ), child: BottomNavigationBar( elevation: 0, enableFeedback: false, backgroundColor: Colors.transparent, selectedLabelStyle: TextStyle( fontWeight: FontWeight.w600, fontSize: 14.sp, ), unselectedLabelStyle: TextStyle( fontWeight: FontWeight.w500, fontSize: 13.sp, ), type: BottomNavigationBarType.fixed, selectedItemColor: Color(0xffBF854A), unselectedItemColor: Color(0xffC4C4C4), showUnselectedLabels: true, showSelectedLabels: true, items: _bottomItems, currentIndex: _currentIndex, onTap: _switchBottomTab, ), ), ), ), ), ], ), ); } ///双击退出 int _lastClickTime = 0; Future _doubleExit() { int nowTime = DateTime.now().microsecondsSinceEpoch; if (_lastClickTime != 0 && nowTime - _lastClickTime > 1500) { return Future.value(true); } else { _lastClickTime = DateTime.now().microsecondsSinceEpoch; Future.delayed(const Duration(milliseconds: 1500), () { _lastClickTime = 0; }); return Future.value(false); } } void _initializePages() { if (_pages.isNotEmpty) { return; } _pages.add(SCIndexHomePage()); _pages.add( TaskPage( showBackButton: false, onTaskStatusChanged: () => unawaited(_loadTaskClaimableCount()), ), ); _pages.add(SCMessagePage()); _pages.add(MePage2()); } Widget _buildCachedBody() { return IndexedStack( index: _currentIndex, children: List.generate(_pages.length, (index) { if (!_builtPageIndexes.contains(index)) { return const SizedBox.shrink(); } return TickerMode( enabled: index == _currentIndex, child: _pages[index], ); }), ); } void _switchBottomTab(int index) { if (index == 1) { unawaited(_loadTaskClaimableCount()); } if (index == _currentIndex) { return; } setState(() { _currentIndex = index; _builtPageIndexes.add(index); }); } void _rebuildBottomItemsIfNeeded() { final locale = Localizations.localeOf(context); if (_lastLocale == locale && _bottomItems.isNotEmpty) { return; } _lastLocale = locale; _bottomItems.clear(); _bottomItems.add( BottomNavigationBarItem( icon: _buildBottomTabIcon( active: false, svgaPath: "sc_images/index/sc_icon_home_anim.svga", fallbackPath: "sc_images/index/sc_icon_home_no.png", ), activeIcon: _buildBottomTabIcon( active: true, svgaPath: "sc_images/index/sc_icon_home_anim.svga", fallbackPath: "sc_images/index/sc_icon_home_en.png", ), label: SCAppLocalizations.of(context)!.home, ), ); _bottomItems.add( BottomNavigationBarItem( icon: _buildTaskTabIcon(active: false), activeIcon: _buildTaskTabIcon(active: true), label: SCAppLocalizations.of(context)!.task, ), ); _bottomItems.add( BottomNavigationBarItem( icon: Selector( selector: (c, p) => p.allUnReadCount, shouldRebuild: (prev, next) => prev != next, builder: (_, allUnReadCount, __) { return allUnReadCount > 0 ? Badge( backgroundColor: Colors.red, label: text( "${allUnReadCount > 99 ? "99+" : allUnReadCount}", fontSize: 9.sp, textColor: Colors.white, fontWeight: FontWeight.w600, ), alignment: AlignmentDirectional.topEnd, child: _buildBottomTabIcon( active: false, svgaPath: "sc_images/index/sc_icon_message_anim.svga", fallbackPath: "sc_images/index/sc_icon_message_no.png", ), ) : _buildBottomTabIcon( active: false, svgaPath: "sc_images/index/sc_icon_message_anim.svga", fallbackPath: "sc_images/index/sc_icon_message_no.png", ); }, ), activeIcon: Selector( selector: (c, p) => p.allUnReadCount, shouldRebuild: (prev, next) => prev != next, builder: (_, allUnReadCount, __) { return allUnReadCount > 0 ? Badge( backgroundColor: Colors.red, label: text( "${allUnReadCount > 99 ? "99+" : allUnReadCount}", fontSize: 9.sp, textColor: Colors.white, fontWeight: FontWeight.w600, ), alignment: AlignmentDirectional.topEnd, child: _buildBottomTabIcon( active: true, svgaPath: "sc_images/index/sc_icon_message_anim.svga", fallbackPath: "sc_images/index/sc_icon_message_en.png", ), ) : _buildBottomTabIcon( active: true, svgaPath: "sc_images/index/sc_icon_message_anim.svga", fallbackPath: "sc_images/index/sc_icon_message_en.png", ); }, ), label: SCAppLocalizations.of(context)!.message, ), ); _bottomItems.add( BottomNavigationBarItem( icon: _buildBottomTabIcon( active: false, svgaPath: "sc_images/index/sc_icon_me_anim.svga", fallbackPath: "sc_images/index/sc_icon_me_no.png", ), activeIcon: _buildBottomTabIcon( active: true, svgaPath: "sc_images/index/sc_icon_me_anim.svga", fallbackPath: "sc_images/index/sc_icon_me_en.png", ), label: SCAppLocalizations.of(context)!.me, ), ); } Future _loadTaskClaimableCount() async { try { final count = await SCAccountRepository().taskClaimableCount(); if (!mounted) return; _taskClaimableCount.value = count; } catch (error) { if (!mounted) return; _taskClaimableCount.value = 0; } } Future _showEntryDialogs() async { if (_hasShownEntryDialogs || !mounted) { return; } _hasShownEntryDialogs = true; await _waitForRegisterRewardSocketIfNeeded(); if (!mounted) { return; } await _showRegisterRewardDialogIfNeeded(); if (!mounted) { return; } await _showEntryPopupIfNeeded(); if (!mounted) { return; } final dialogData = await _loadDailySignInDialogData(); if (!mounted || dialogData == null) { await _openFirstRegisterRoomGameIfNeeded(); return; } _isShowingDailySignInDialog = true; await DailySignInDialog.show(context, data: dialogData); _isShowingDailySignInDialog = false; if (!mounted) { return; } if (_showRegisterRewardAfterDailySignIn) { _showRegisterRewardAfterDailySignIn = false; await _showRegisterRewardDialogIfNeeded(); } if (!mounted) { return; } await _openFirstRegisterRoomGameIfNeeded(); } Future _showEntryPopupIfNeeded() async { if (!mounted || !(ModalRoute.of(context)?.isCurrent ?? false)) { return; } await SCEntryPopupCoordinator.showIfNeeded( context, onSelectRootTab: _switchBottomTab, ); } Future _waitForRegisterRewardSocketIfNeeded() async { if (!DataPersistence.getAwaitRegisterRewardSocket() || DataPersistence.getPendingRegisterRewardDialog()) { return; } final completer = Completer(); _registerRewardSocketCompleter = completer; await Future.any([ completer.future, Future.delayed(_registerRewardSocketWaitTimeout), ]); if (identical(_registerRewardSocketCompleter, completer)) { _registerRewardSocketCompleter = null; } if (!DataPersistence.getPendingRegisterRewardDialog()) { await DataPersistence.clearAwaitRegisterRewardSocket(); } } Future _showRegisterRewardDialogIfNeeded() async { if (_hasShownRegisterRewardDialog || !DataPersistence.getPendingRegisterRewardDialog()) { return; } await DataPersistence.clearPendingRegisterRewardDialog(); await DataPersistence.clearAwaitRegisterRewardSocket(); if (!mounted) { return; } _hasShownRegisterRewardDialog = true; await RegisterRewardDialog.show(context); } void _onRegisterRewardGranted(RegisterRewardGrantedEvent event) { final completer = _registerRewardSocketCompleter; if (completer != null && !completer.isCompleted) { completer.complete(); } if (!mounted || _hasShownRegisterRewardDialog) { return; } if (_isShowingDailySignInDialog) { _showRegisterRewardAfterDailySignIn = true; return; } if (_hasShownEntryDialogs) { unawaited(_showRegisterRewardDialogIfNeeded()); } } Future _loadDailySignInDialogData() async { try { final statusRes = await SCAccountRepository().signInRewardStatus(); if (!statusRes.canShowDialog) { return null; } final dialogData = DailySignInDialogData.fromStatus(statusRes: statusRes); return dialogData; } catch (error) { return null; } } Future _openFirstRegisterRoomGameIfNeeded() async { if (_isOpeningFirstRegisterRoomGame || !DataPersistence.getPendingFirstRegisterRoomGameEvent()) { return; } if (!mounted) { return; } _isOpeningFirstRegisterRoomGame = true; try { await DataPersistence.clearPendingFirstRegisterRoomGameEvent(); if (!mounted) { return; } await SCEntryPopupCoordinator.openFirstPartyRoomRandomGame(context); } catch (_) { // The entry popup is optional; failure should not block the home page. } finally { _isOpeningFirstRegisterRoomGame = false; } } Widget _buildBottomTabIcon({ required bool active, required String svgaPath, required String fallbackPath, }) { return SCSvgaAssetWidget( assetPath: svgaPath, width: 35.w, height: 35.w, active: active, loop: false, fallback: Image.asset(fallbackPath, width: 35.w, height: 35.w), ); } Widget _buildTaskTabIcon({required bool active}) { return ValueListenableBuilder( valueListenable: _taskClaimableCount, builder: (_, claimableCount, __) { final icon = _buildBottomTabIcon( active: active, svgaPath: "sc_images/index/sc_icon_explore_anim.svga", fallbackPath: active ? "sc_images/index/sc_icon_explore_en.png" : "sc_images/index/sc_icon_explore_no.png", ); if (claimableCount <= 0) { return icon; } return Badge( backgroundColor: Colors.red, label: text( claimableCount > 99 ? "99+" : "$claimableCount", fontSize: 9.sp, textColor: Colors.white, fontWeight: FontWeight.w600, ), alignment: AlignmentDirectional.topEnd, child: icon, ); }, ); } }