import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:provider/provider.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import '../../../../app_localizations.dart'; import '../../../../app/constants/sc_global_config.dart'; import '../../../../app/routes/sc_fluro_navigator.dart'; import '../../../../shared/business_logic/models/res/sc_index_banner_res.dart'; import '../../../../shared/tools/sc_banner_utils.dart'; import '../../../../shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import '../../../../shared/business_logic/models/res/follow_room_res.dart'; import '../../../../shared/business_logic/models/res/room_res.dart'; import '../../../../services/audio/rtc_manager.dart'; import '../../../../services/general/sc_app_general_manager.dart'; import '../../../../ui_kit/components/sc_compontent.dart'; import '../../../../ui_kit/components/text/sc_text.dart'; import '../../../../ui_kit/widgets/room/room_live_audio_indicator.dart'; import '../../../index/main_route.dart'; const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450); const Color _kPartySkeletonShell = Color(0xFF0F3730); const Color _kPartySkeletonShellStrong = Color(0xFF1A4A41); const Color _kPartySkeletonBoneBase = Color(0xFF2B5C53); const Color _kPartySkeletonBoneHighlight = Color(0xFF5F8177); const Color _kPartySkeletonBorder = Color(0x66D8B57B); class SCHomePartyPage extends StatefulWidget { const SCHomePartyPage({super.key}); @override State createState() => _HomePartyPageState(); } class _HomePartyPageState extends State with SingleTickerProviderStateMixin { List historyRooms = []; String? lastId; bool isLoading = false; bool _isBannerLoading = false; bool _isLeaderboardLoading = false; final RefreshController _refreshController = RefreshController( initialRefresh: false, ); late final AnimationController _skeletonController; List rooms = []; int _currentIndex = 0; @override void initState() { super.initState(); _skeletonController = AnimationController( vsync: this, duration: _kPartySkeletonAnimationDuration, )..repeat(); loadData(); } @override void dispose() { _skeletonController.dispose(); _refreshController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, body: Stack( alignment: AlignmentDirectional.bottomCenter, children: [ SmartRefresher( enablePullDown: true, enablePullUp: false, controller: _refreshController, onRefresh: () { loadData(); }, onLoading: () {}, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Consumer( builder: (context, ref, child) { if (!_shouldShowBannerSection(ref)) { return const SizedBox.shrink(); } return Padding( padding: EdgeInsets.only(bottom: 12.w), child: Stack( alignment: AlignmentDirectional.center, children: [ if (_shouldShowBannerSkeleton(ref)) _buildBannerSkeleton() else _banner(ref), ], ), ); }, ), Consumer( builder: (context, ref, child) { if (_shouldShowLeaderboardSkeleton(ref)) { return _buildLeaderboardSkeleton(); } return _banner2(ref); }, ), _buildRoomsSection(), ], ), ), ), ], ), ); } _banner(SCAppGeneralManager ref) { final banners = _partyBanners(ref); final hasMultipleBanners = banners.length > 1; if (banners.isEmpty) { return Container(); } return Stack( alignment: AlignmentDirectional.bottomCenter, children: [ CarouselSlider( options: CarouselOptions( height: 95.w, autoPlay: hasMultipleBanners, // 启用自动播放 enlargeCenterPage: false, // 居中放大当前页面 enableInfiniteScroll: hasMultipleBanners, // 启用无限循环 autoPlayAnimationDuration: Duration(milliseconds: 800), // 自动播放动画时长 viewportFraction: 1, scrollPhysics: hasMultipleBanners ? const BouncingScrollPhysics() : const NeverScrollableScrollPhysics(), // 视口分数 onPageChanged: (index, reason) { _currentIndex = index; setState(() {}); }, ), items: banners.map((item) { return GestureDetector( child: netImage( url: item.cover ?? "", height: 95.w, width: ScreenUtil().screenWidth * 0.9, fit: BoxFit.fill, borderRadius: BorderRadius.circular(12.w), ), onTap: () { SCBannerUtils.openBanner(item, context); }, ); }).toList(), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: banners.asMap().entries.map((entry) { return Container( width: 6.0, height: 6.0, margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), decoration: BoxDecoration( shape: BoxShape.circle, color: _currentIndex == entry.key ? Colors.blue : Colors.white, ), ); }).toList(), ), ], ); } List _partyBanners(SCAppGeneralManager ref) { if (ref.exploreBanners.isNotEmpty) { return ref.exploreBanners; } return ref.homeBanners; } _banner2(SCAppGeneralManager ref) { String roomGiftOneAvatar = ""; String roomGiftTwoAvatar = ""; String roomGiftThreeAvatar = ""; if (ref.appLeaderResult?.roomGiftsLeaderboard != null && ref.appLeaderResult?.roomGiftsLeaderboard?.weekly != null) { if ((ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.isNotEmpty ?? false) && ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.first != null) { roomGiftOneAvatar = ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.first.avatar ?? ""; } if (ref.appLeaderResult!.roomGiftsLeaderboard!.weekly!.length > 1 && ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?[1] != null) { roomGiftTwoAvatar = ref.appLeaderResult?.roomGiftsLeaderboard?.weekly![1].avatar ?? ""; } if (ref.appLeaderResult!.roomGiftsLeaderboard!.weekly!.length > 2 && ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?[2] != null) { roomGiftThreeAvatar = ref.appLeaderResult?.roomGiftsLeaderboard?.weekly![2].avatar ?? ""; } } String wealthOneAvatar = ""; String wealthTwoAvatar = ""; String wealthThreeAvatar = ""; if (ref.appLeaderResult?.giftsSendLeaderboard != null && ref.appLeaderResult?.giftsSendLeaderboard?.weekly != null) { if ((ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.isNotEmpty ?? false) && ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.first != null) { wealthOneAvatar = ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.first.avatar ?? ""; } if (ref.appLeaderResult!.giftsSendLeaderboard!.weekly!.length > 1 && ref.appLeaderResult?.giftsSendLeaderboard?.weekly?[1] != null) { wealthTwoAvatar = ref.appLeaderResult?.giftsSendLeaderboard?.weekly![1].avatar ?? ""; } if (ref.appLeaderResult!.giftsSendLeaderboard!.weekly!.length > 2 && ref.appLeaderResult?.giftsSendLeaderboard?.weekly?[2] != null) { wealthThreeAvatar = ref.appLeaderResult?.giftsSendLeaderboard?.weekly![2].avatar ?? ""; } } String charmOneAvatar = ""; String charmTwoAvatar = ""; String charmThreeAvatar = ""; if (ref.appLeaderResult?.giftsReceivedLeaderboard != null && ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly != null) { if ((ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?.isNotEmpty ?? false) && ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?.first != null) { charmOneAvatar = ref .appLeaderResult ?.giftsReceivedLeaderboard ?.weekly ?.first .avatar ?? ""; } if (ref.appLeaderResult!.giftsReceivedLeaderboard!.weekly!.length > 1 && ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?[1] != null) { charmTwoAvatar = ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly![1].avatar ?? ""; } if (ref.appLeaderResult!.giftsReceivedLeaderboard!.weekly!.length > 2 && ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?[2] != null) { charmThreeAvatar = ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly![2].avatar ?? ""; } } if (roomGiftOneAvatar.isEmpty && wealthOneAvatar.isEmpty && charmOneAvatar.isEmpty) { return Container(); } return SizedBox( height: 130.w, child: Row( children: [ Expanded( child: GestureDetector( child: Stack( alignment: AlignmentDirectional.center, children: [ PositionedDirectional( start: 26.w, top: 65.w, child: head(url: wealthTwoAvatar, width: 30.w), ), Positioned( top: 35.w, child: head(url: wealthOneAvatar, width: 36.w), ), PositionedDirectional( end: 24.w, top: 65.w, child: head(url: wealthThreeAvatar, width: 30.w), ), Image.asset( SCGlobalConfig.businessLogicStrategy .getPopularLeaderboardBackgroundImage('wealth'), fit: BoxFit.fill, ), ], ), onTap: () { SCNavigatorUtils.push( context, "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.wealthRankUrl)}&showTitle=false", replace: false, ); }, ), ), Expanded( child: GestureDetector( child: Stack( alignment: AlignmentDirectional.center, children: [ PositionedDirectional( start: 24.w, top: 68.w, child: head(url: roomGiftTwoAvatar, width: 30.w), ), Positioned( top: 35.w, child: head(url: roomGiftOneAvatar, width: 38.w), ), PositionedDirectional( end: 25.w, top: 69.w, child: head(url: roomGiftThreeAvatar, width: 30.w), ), Image.asset( SCGlobalConfig.businessLogicStrategy .getPopularLeaderboardBackgroundImage('room'), fit: BoxFit.fill, ), ], ), onTap: () { SCNavigatorUtils.push( context, "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.roomRankUrl)}&showTitle=false", replace: false, ); }, ), ), Expanded( child: GestureDetector( child: Stack( alignment: AlignmentDirectional.center, children: [ PositionedDirectional( start: 26.w, top: 66.w, child: head(url: charmTwoAvatar, width: 30.w), ), Positioned( top: 38.w, child: head(url: charmOneAvatar, width: 37.w), ), PositionedDirectional( end: 25.w, top: 66.w, child: head(url: charmThreeAvatar, width: 30.w), ), Image.asset( SCGlobalConfig.businessLogicStrategy .getPopularLeaderboardBackgroundImage('charm'), fit: BoxFit.fill, ), ], ), onTap: () { SCNavigatorUtils.push( context, "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.charmRankUrl)}&showTitle=false", replace: false, ); }, ), ), ], ), ); } Widget _buildRoomsSection() { if (isLoading && rooms.isEmpty) { return _buildRoomGridSkeleton(); } if (rooms.isEmpty) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { loadData(); }, child: mainEmpty(msg: SCAppLocalizations.of(context)!.noData), ); } return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 1, mainAxisSpacing: 10, crossAxisSpacing: 10, ), padding: EdgeInsets.all(10), itemCount: rooms.length, itemBuilder: (context, index) { return _buildItem(rooms[index], index); }, ); } bool _shouldShowLeaderboardSkeleton(SCAppGeneralManager ref) { return _isLeaderboardLoading && !_hasLeaderboardData(ref); } bool _shouldShowBannerSection(SCAppGeneralManager ref) { return _shouldShowBannerSkeleton(ref) || _partyBanners(ref).isNotEmpty; } bool _shouldShowBannerSkeleton(SCAppGeneralManager ref) { return _isBannerLoading && _partyBanners(ref).isEmpty; } bool _hasLeaderboardData(SCAppGeneralManager ref) { final appLeaderResult = ref.appLeaderResult; return (appLeaderResult?.giftsSendLeaderboard?.weekly?.isNotEmpty ?? false) || (appLeaderResult?.roomGiftsLeaderboard?.weekly?.isNotEmpty ?? false) || (appLeaderResult?.giftsReceivedLeaderboard?.weekly?.isNotEmpty ?? false); } Widget _buildBannerSkeleton() { return Padding( padding: EdgeInsets.symmetric(horizontal: 18.w), child: SizedBox( height: 95.w, child: Stack( alignment: Alignment.bottomCenter, children: [ Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.w), border: Border.all(color: _kPartySkeletonBorder, width: 1.w), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.14), blurRadius: 18.w, offset: Offset(0, 8.w), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12.w), child: Stack( children: [ Positioned.fill( child: Opacity( opacity: 0.45, child: _buildSkeletonPanel(), ), ), ], ), ), ), ), Positioned( bottom: 10.w, child: Row( children: List.generate(3, (index) { return Padding( padding: EdgeInsets.symmetric(horizontal: 4.w), child: _buildSkeletonBone( width: 6.w, height: 6.w, shape: BoxShape.circle, ), ); }), ), ), ], ), ), ); } Widget _buildLeaderboardSkeleton() { return SizedBox( height: 130.w, child: Row( children: List.generate(3, (index) { final bool isCenterCard = index == 1; return Expanded( child: Padding( padding: EdgeInsets.fromLTRB( 4.w, isCenterCard ? 0 : 6.w, 4.w, isCenterCard ? 0 : 6.w, ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(18.w), border: Border.all(color: _kPartySkeletonBorder, width: 1.w), gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.18), blurRadius: 14.w, offset: Offset(0, 8.w), ), ], ), child: Stack( alignment: Alignment.center, children: [ Positioned.fill( child: Padding( padding: EdgeInsets.all(1.w), child: ClipRRect( borderRadius: BorderRadius.circular(17.w), child: Opacity( opacity: 0.34, child: _buildSkeletonPanel(), ), ), ), ), Positioned( top: 21.w, child: _buildSkeletonBone( width: isCenterCard ? 42.w : 38.w, height: isCenterCard ? 42.w : 38.w, shape: BoxShape.circle, ), ), PositionedDirectional( start: 16.w, top: 56.w, child: _buildSkeletonBone( width: 28.w, height: 28.w, shape: BoxShape.circle, ), ), PositionedDirectional( end: 16.w, top: 56.w, child: _buildSkeletonBone( width: 28.w, height: 28.w, shape: BoxShape.circle, ), ), Positioned( bottom: 16.w, child: _buildSkeletonBone( width: isCenterCard ? 56.w : 48.w, height: 10.w, borderRadius: BorderRadius.circular(999.w), ), ), ], ), ), ), ); }), ), ); } Widget _buildRoomGridSkeleton() { return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 1, mainAxisSpacing: 10, crossAxisSpacing: 10, ), padding: EdgeInsets.all(10.w), itemCount: 6, itemBuilder: (context, index) => _buildRoomSkeletonCard(), ); } Widget _buildRoomSkeletonCard() { return Container( margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), decoration: BoxDecoration( borderRadius: BorderRadius.circular(14.w), border: Border.all(color: _kPartySkeletonBorder, width: 1.w), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.16), blurRadius: 16.w, offset: Offset(0, 8.w), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(14.w), child: Stack( alignment: Alignment.bottomCenter, children: [ Positioned.fill( child: DecoratedBox( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], ), ), ), ), Positioned.fill( child: Padding( padding: EdgeInsets.only(bottom: 34.w), child: _buildSkeletonPanel( borderRadius: BorderRadius.vertical( top: Radius.circular(14.w), ), ), ), ), Positioned( top: 12.w, right: 12.w, child: _buildSkeletonBone( width: 30.w, height: 12.w, borderRadius: BorderRadius.circular(999.w), ), ), Positioned( left: 0, right: 0, bottom: 0, child: Container( height: 34.w, padding: EdgeInsets.symmetric(horizontal: 10.w), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.24), border: Border( top: BorderSide( color: Colors.white.withValues(alpha: 0.05), width: 1.w, ), ), ), child: Row( children: [ _buildSkeletonBone( width: 20.w, height: 13.w, borderRadius: BorderRadius.circular(3.w), ), SizedBox(width: 6.w), Expanded( child: _buildSkeletonBone( height: 10.w, borderRadius: BorderRadius.circular(999.w), ), ), SizedBox(width: 8.w), _buildSkeletonBone( width: 18.w, height: 10.w, borderRadius: BorderRadius.circular(999.w), ), ], ), ), ), ], ), ), ); } Widget _buildSkeletonPanel({ BorderRadius? borderRadius, BoxShape shape = BoxShape.rectangle, }) { return AnimatedBuilder( animation: _skeletonController, builder: (context, child) { final double slideOffset = (_skeletonController.value * 2) - 1; return DecoratedBox( decoration: BoxDecoration( shape: shape, borderRadius: shape == BoxShape.circle ? null : (borderRadius ?? BorderRadius.circular(12.w)), gradient: LinearGradient( begin: Alignment(-1.4 + slideOffset, -0.2), end: Alignment(1.4 + slideOffset, 0.2), colors: const [ _kPartySkeletonBoneBase, _kPartySkeletonBoneBase, _kPartySkeletonBoneHighlight, _kPartySkeletonBoneBase, ], stops: const [0.0, 0.36, 0.54, 1.0], ), ), ); }, ); } Widget _buildSkeletonBone({ double? width, double? height, BorderRadius? borderRadius, BoxShape shape = BoxShape.rectangle, }) { return SizedBox( width: width, height: height, child: _buildSkeletonPanel(borderRadius: borderRadius, shape: shape), ); } loadData() { final generalManager = Provider.of( context, listen: false, ); setState(() { isLoading = true; _isBannerLoading = generalManager.exploreBanners.isEmpty && generalManager.homeBanners.isEmpty; _isLeaderboardLoading = generalManager.appLeaderResult == null; }); SCChatRoomRepository() .discovery(allRegion: true) .then((values) { rooms = values; isLoading = false; _refreshController.refreshCompleted(); _refreshController.loadComplete(); if (mounted) setState(() {}); }) .catchError((e) { _refreshController.loadNoData(); _refreshController.refreshCompleted(); isLoading = false; if (mounted) setState(() {}); }); generalManager.loadMainBanner().whenComplete(() { if (!mounted) { return; } setState(() { _isBannerLoading = false; }); }); generalManager.appLeaderboard().whenComplete(() { if (!mounted) { return; } setState(() { _isLeaderboardLoading = false; }); }); } _buildItem(SocialChatRoomRes res, int index) { return SCDebounceWidget( child: Container( margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.w), color: Colors.transparent, ), child: Stack( alignment: Alignment.bottomCenter, children: [ Container( padding: EdgeInsets.all(3.w), decoration: BoxDecoration( image: DecorationImage( image: AssetImage("sc_images/index/sc_icon_room_bord.png"), fit: BoxFit.fill, ), ), child: netImage( url: resolveRoomCoverUrl(res.id, res.roomCover), defaultImg: kRoomCoverDefaultImg, borderRadius: BorderRadius.circular(12.w), width: 200.w, height: 200.w, ), ), Container( padding: EdgeInsets.symmetric(vertical: 6.w), margin: EdgeInsets.symmetric(horizontal: 1.w), decoration: BoxDecoration( image: DecorationImage( image: AssetImage( "sc_images/index/sc_icon_index_room_brd.png", ), fit: BoxFit.fill, ), ), child: Row( children: [ SizedBox(width: 10.w), Consumer( builder: (_, provider, __) { return netImage( url: "${provider.findCountryByName(res.countryName ?? "")?.nationalFlag}", width: 20.w, height: 13.w, borderRadius: BorderRadius.circular(2.w), ); }, ), SizedBox(width: 5.w), Expanded( child: SizedBox( height: 17.w, child: Align( alignment: Alignment.centerLeft, child: Transform.translate( offset: Offset(0, -0.6.w), child: text( res.roomName ?? "", fontSize: 13.sp, textColor: Color(0xffffffff), fontWeight: FontWeight.w400, lineHeight: 1, ), ), ), // (roomRes.roomProfile?.roomName?.length ?? 0) > 10 // ? Marquee( // text: roomRes.roomProfile?.roomName ?? "", // style: TextStyle( // fontSize: 15.sp, // color: Color(0xffffffff), // fontWeight: FontWeight.w400, // decoration: TextDecoration.none, // ), // scrollAxis: Axis.horizontal, // crossAxisAlignment: CrossAxisAlignment.start, // blankSpace: 20.0, // velocity: 40.0, // pauseAfterRound: Duration(seconds: 1), // accelerationDuration: Duration(seconds: 1), // accelerationCurve: Curves.easeOut, // decelerationDuration: Duration( // milliseconds: 500, // ), // decelerationCurve: Curves.easeOut, // ) // : Text( // roomRes.roomProfile?.roomName ?? "", // maxLines: 1, // overflow: TextOverflow.ellipsis, // style: TextStyle( // fontSize: 15.sp, // color: Color(0xffffffff), // fontWeight: FontWeight.w400, // decoration: TextDecoration.none, // ), // ), ), ), SizedBox(width: 5.w), (res.extValues?.roomSetting?.password?.isEmpty ?? false) ? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w) : Image.asset( "sc_images/index/sc_icon_room_suo.png", width: 20.w, height: 20.w, ), (res.extValues?.roomSetting?.password?.isEmpty ?? false) ? SizedBox(width: 3.w) : Container(height: 10.w), (res.extValues?.roomSetting?.password?.isEmpty ?? false) ? text( res.extValues?.memberQuantity ?? "0", fontSize: 10.sp, lineHeight: 1, ) : Container(height: 10.w), SizedBox(width: 10.w), ], ), ), ], ), ), onTap: () { Provider.of( context, listen: false, ).joinVoiceRoomSession(context, res.id ?? ""); }, ); } }