yumi-flutter/lib/modules/home/popular/party/sc_home_party_page.dart

1563 lines
51 KiB
Dart

import 'package:carousel_slider/carousel_slider.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svg/flutter_svg.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 '../../../../services/home/home_room_preload_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);
const Color _kPartyAccent = Color(0xFF18F2B1);
class _RoomCountryFilterOption {
const _RoomCountryFilterOption({
required this.key,
required this.name,
this.flagUrl,
});
final String key;
final String name;
final String? flagUrl;
}
class SCHomePartyPage extends StatefulWidget {
const SCHomePartyPage({super.key});
@override
State<SCHomePartyPage> createState() => _HomePartyPageState();
}
class _HomePartyPageState extends State<SCHomePartyPage>
with
SingleTickerProviderStateMixin,
WidgetsBindingObserver,
AutomaticKeepAliveClientMixin {
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
static const Duration _roomCounterHydrationMinGap = Duration(seconds: 20);
static const int _roomCounterHydrationLimit = 6;
static const String _wealthRankBackgroundAsset =
'sc_images/index/sc_party_rank_wealth_bg.png';
static const String _charmRankBackgroundAsset =
'sc_images/index/sc_party_rank_charm_bg.png';
static const String _roomRankBackgroundAsset =
'sc_images/index/sc_party_rank_room_bg.png';
static const String _rankFrameGoldAsset =
'sc_images/index/sc_party_rank_frame_gold.png';
static const String _rankFrameSilverAsset =
'sc_images/index/sc_party_rank_frame_silver.png';
static const String _rankFrameBronzeAsset =
'sc_images/index/sc_party_rank_frame_bronze.png';
static const String _rankRibbonsAsset =
'sc_images/index/sc_party_rank_ribbons.svg';
List<FollowRoomRes> historyRooms = [];
String? lastId;
bool isLoading = false;
bool _isBannerLoading = false;
bool _isLeaderboardLoading = false;
final RefreshController _refreshController = RefreshController(
initialRefresh: false,
);
late final AnimationController _skeletonController;
List<SocialChatRoomRes> rooms = [];
int _currentIndex = 0;
Timer? _roomListRefreshTimer;
bool _isSilentRefreshingRooms = false;
bool _isHydratingVisibleRoomCounters = false;
bool _wasTickerModeEnabled = false;
bool _hasCompletedInitialRoomLoad = false;
DateTime? _lastRoomCounterHydrationAt;
String? _selectedCountryKey;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_skeletonController = AnimationController(
vsync: this,
duration: _kPartySkeletonAnimationDuration,
)..repeat();
loadData();
_startRoomListAutoRefresh();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_roomListRefreshTimer?.cancel();
_skeletonController.dispose();
_refreshController.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_refreshRoomsSilently();
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final isTickerModeEnabled = TickerMode.valuesOf(context).enabled;
if (isTickerModeEnabled && !_wasTickerModeEnabled) {
_refreshRoomsSilently();
}
_wasTickerModeEnabled = isTickerModeEnabled;
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent,
body: Stack(
alignment: AlignmentDirectional.bottomCenter,
children: [
SmartRefresher(
enablePullDown: true,
enablePullUp: false,
controller: _refreshController,
onRefresh: () {
loadData(forceRefresh: true);
},
onLoading: () {},
child: SingleChildScrollView(
padding: EdgeInsets.only(top: 10.w, bottom: 10.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Consumer<SCAppGeneralManager>(
builder: (context, ref, child) {
if (!_shouldShowBannerSection(ref)) {
return const SizedBox.shrink();
}
return Padding(
padding: EdgeInsets.only(bottom: 8.w),
child: Stack(
alignment: AlignmentDirectional.center,
children: [
if (_shouldShowBannerSkeleton(ref))
_buildBannerSkeleton()
else
_banner(ref),
],
),
);
},
),
Consumer<SCAppGeneralManager>(
builder: (context, ref, child) {
if (_shouldShowLeaderboardSkeleton(ref)) {
return _buildLeaderboardSkeleton();
}
return _banner2(ref);
},
),
_buildRoomsSection(),
],
),
),
),
],
),
);
}
void _startRoomListAutoRefresh() {
_roomListRefreshTimer?.cancel();
_roomListRefreshTimer = Timer.periodic(_roomListRefreshInterval, (_) {
_refreshRoomsSilently();
});
}
bool _canRefreshRoomListSilently() {
return mounted &&
TickerMode.valuesOf(context).enabled &&
!_isSilentRefreshingRooms &&
!isLoading;
}
bool _sameRooms(
List<SocialChatRoomRes> previous,
List<SocialChatRoomRes> next,
) {
if (previous.length != next.length) {
return false;
}
for (int i = 0; i < previous.length; i++) {
if (!_sameRoom(previous[i], next[i])) {
return false;
}
}
return true;
}
bool _sameRoom(SocialChatRoomRes previous, SocialChatRoomRes next) {
return previous.id == next.id &&
previous.countryCode == next.countryCode &&
previous.countryName == next.countryName &&
previous.nationalFlag == next.nationalFlag &&
previous.roomName == next.roomName &&
previous.roomCover == next.roomCover &&
previous.roomGameIcon == next.roomGameIcon &&
previous.displayMemberCount == next.displayMemberCount &&
previous.extValues?.existsPassword == next.extValues?.existsPassword;
}
List<SocialChatRoomRes> _mergeLatestRoomsWithCurrentCounters(
List<SocialChatRoomRes> latestRooms,
) {
if (rooms.isEmpty) {
return latestRooms;
}
final currentRoomById = {
for (final room in rooms)
if ((room.id ?? "").isNotEmpty) room.id!: room,
};
return latestRooms.map((room) {
final roomId = room.id;
if ((roomId ?? "").isEmpty) {
return room;
}
final currentRoom = currentRoomById[roomId];
if (currentRoom == null || room.roomCounter != null) {
return room;
}
return room.copyWith(roomCounter: currentRoom.roomCounter);
}).toList();
}
List<SocialChatRoomRes> _filteredRooms() {
final selectedCountryKey = _selectedCountryKey;
if ((selectedCountryKey ?? "").isEmpty) {
return rooms;
}
return rooms
.where((room) => _countryFilterKeyForRoom(room) == selectedCountryKey)
.toList();
}
List<_RoomCountryFilterOption> _roomCountryFilterOptions() {
final options = <_RoomCountryFilterOption>[];
final seenKeys = <String>{};
for (final room in rooms) {
final key = _countryFilterKeyForRoom(room);
final name = _countryFilterNameForRoom(room);
if (key == null || name == null || seenKeys.contains(key)) {
continue;
}
seenKeys.add(key);
options.add(
_RoomCountryFilterOption(
key: key,
name: name,
flagUrl: _trimToNull(room.nationalFlag),
),
);
}
return options;
}
bool _syncCountryFilterWithRooms() {
final selectedCountryKey = _selectedCountryKey;
if ((selectedCountryKey ?? "").isEmpty) {
return false;
}
final hasSelectedCountry = rooms.any(
(room) => _countryFilterKeyForRoom(room) == selectedCountryKey,
);
if (hasSelectedCountry) {
return false;
}
_selectedCountryKey = null;
return true;
}
String? _countryFilterKeyForRoom(SocialChatRoomRes room) {
final countryName = _trimToNull(room.countryName);
if (countryName != null) {
return countryName.toLowerCase();
}
final countryCode = _trimToNull(room.countryCode);
return countryCode?.toUpperCase();
}
String? _countryFilterNameForRoom(SocialChatRoomRes room) {
return _trimToNull(room.countryName) ?? _trimToNull(room.countryCode);
}
String? _trimToNull(String? value) {
final trimmed = value?.trim();
if (trimmed == null || trimmed.isEmpty) {
return null;
}
return trimmed;
}
Future<void> _syncVisibleRoomMemberCounts({bool force = false}) async {
if (!mounted || rooms.isEmpty || _isHydratingVisibleRoomCounters) {
return;
}
final now = DateTime.now();
if (!force &&
_lastRoomCounterHydrationAt != null &&
now.difference(_lastRoomCounterHydrationAt!) <
_roomCounterHydrationMinGap) {
return;
}
final targetRoomIds =
_filteredRooms()
.take(_roomCounterHydrationLimit)
.map((room) => room.id ?? "")
.where((roomId) => roomId.trim().isNotEmpty)
.toList();
if (targetRoomIds.isEmpty) {
return;
}
_isHydratingVisibleRoomCounters = true;
_lastRoomCounterHydrationAt = now;
try {
final hydratedCounters = await Future.wait(
targetRoomIds.map((roomId) async {
try {
final specificRoom = await SCChatRoomRepository().specific(roomId);
final counter = specificRoom.counter;
return MapEntry(
roomId,
counter == null
? null
: RoomMemberCounter(
adminCount: counter.adminCount,
memberCount: counter.memberCount,
),
);
} catch (_) {
return MapEntry<String, RoomMemberCounter?>(roomId, null);
}
}),
);
if (!mounted) {
return;
}
final roomIndexById = {
for (int i = 0; i < rooms.length; i++)
if ((rooms[i].id ?? "").isNotEmpty) rooms[i].id!: i,
};
final nextRooms = List<SocialChatRoomRes>.from(rooms);
bool changed = false;
for (final entry in hydratedCounters) {
final roomIndex = roomIndexById[entry.key];
final roomCounter = entry.value;
if (roomIndex == null || roomCounter == null) {
continue;
}
final currentRoom = nextRooms[roomIndex];
final nextRoom = currentRoom.copyWith(roomCounter: roomCounter);
if (_sameRoom(currentRoom, nextRoom)) {
continue;
}
nextRooms[roomIndex] = nextRoom;
changed = true;
}
if (changed) {
setState(() {
rooms = nextRooms;
});
}
} finally {
_isHydratingVisibleRoomCounters = false;
}
}
Future<void> _refreshRoomsSilently() async {
if (!_canRefreshRoomListSilently()) {
return;
}
_isSilentRefreshingRooms = true;
try {
final latestRooms = await SCChatRoomRepository().discovery(
allRegion: true,
);
final mergedRooms = _mergeLatestRoomsWithCurrentCounters(latestRooms);
if (!mounted || !TickerMode.valuesOf(context).enabled) {
return;
}
if (_sameRooms(rooms, mergedRooms)) {
unawaited(_syncVisibleRoomMemberCounts());
return;
}
setState(() {
rooms = mergedRooms;
_syncCountryFilterWithRooms();
});
unawaited(_syncVisibleRoomMemberCounts(force: true));
} catch (_) {
} finally {
_isSilentRefreshingRooms = false;
}
}
_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: 100.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: Container(
width: ScreenUtil().screenWidth - 20.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.w),
),
clipBehavior: Clip.antiAlias,
child: netImage(
url: item.cover ?? "",
height: 100.w,
width: ScreenUtil().screenWidth - 20.w,
fit: BoxFit.cover,
borderRadius: BorderRadius.circular(12.w),
),
),
onTap: () {
SCBannerUtils.openBanner(item, context);
},
);
}).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children:
banners.asMap().entries.map((entry) {
return Container(
width: _currentIndex == entry.key ? 10.w : 4.w,
height: 3.w,
margin: EdgeInsets.symmetric(vertical: 6.w, horizontal: 2.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999.w),
color:
_currentIndex == entry.key
? _kPartyAccent
: _kPartyAccent.withValues(alpha: 0.45),
),
);
}).toList(),
),
],
);
}
List<SCIndexBannerRes> _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 Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),
child: Row(
children: [
_buildLargeLeaderboardCard(
title: SCAppLocalizations.of(context)!.leaderboardWealth,
borderColor: const Color(0xFF05C58A),
gradientColors: const [Color(0xFF235045), Color(0xFF152121)],
imageAsset: _wealthRankBackgroundAsset,
avatars: [wealthTwoAvatar, wealthOneAvatar, wealthThreeAvatar],
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.wealthRankUrl)}&showTitle=false",
replace: false,
);
},
),
SizedBox(width: 11.w),
Expanded(
child: Column(
children: [
_buildSmallLeaderboardCard(
title: SCAppLocalizations.of(context)!.leaderboardCharm,
borderColor: const Color(0xFF6800E1),
gradientColors: const [Color(0x99632CBA), Color(0x998747CB)],
imageAsset: _charmRankBackgroundAsset,
imageTop: -3,
imageEnd: 13,
avatars: [charmTwoAvatar, charmOneAvatar, charmThreeAvatar],
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.charmRankUrl)}&showTitle=false",
replace: false,
);
},
),
SizedBox(height: 10.w),
_buildSmallLeaderboardCard(
title: SCAppLocalizations.of(context)!.leaderboardRoom,
borderColor: const Color(0xFF0A88F5),
gradientColors: const [Color(0xCC2169A3), Color(0xCC2F7F9D)],
imageAsset: _roomRankBackgroundAsset,
imageTop: 2,
imageEnd: 8,
avatars: [
roomGiftTwoAvatar,
roomGiftOneAvatar,
roomGiftThreeAvatar,
],
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.roomRankUrl)}&showTitle=false",
replace: false,
);
},
),
],
),
),
],
),
);
}
Widget _buildLargeLeaderboardCard({
required String title,
required Color borderColor,
required List<Color> gradientColors,
required String imageAsset,
required List<String> avatars,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 172.w,
height: 130.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14.w),
border: Border.all(color: borderColor, width: 1.w),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: gradientColors,
),
),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
Positioned(
left: 0,
top: -6.w,
width: 172.w,
height: 172.w,
child: Image.asset(imageAsset, fit: BoxFit.contain),
),
_buildLeaderboardTitle(title, borderColor),
PositionedDirectional(
start: 25.w,
bottom: 6.w,
width: 123.w,
height: 16.w,
child: SvgPicture.asset(_rankRibbonsAsset, fit: BoxFit.fill),
),
PositionedDirectional(
start: 8.w,
bottom: 12.w,
child: _buildLeaderboardAvatar(
avatar: avatars[0],
size: 45.w,
avatarSize: 43.w,
frameAsset: _rankFrameSilverAsset,
),
),
Positioned(
left: 0,
right: 0,
bottom: 20.w,
child: Center(
child: _buildLeaderboardAvatar(
avatar: avatars[1],
size: 55.w,
avatarSize: 50.w,
frameAsset: _rankFrameGoldAsset,
),
),
),
PositionedDirectional(
end: 9.w,
bottom: 12.w,
child: _buildLeaderboardAvatar(
avatar: avatars[2],
size: 45.w,
avatarSize: 43.w,
frameAsset: _rankFrameBronzeAsset,
),
),
],
),
),
);
}
Widget _buildSmallLeaderboardCard({
required String title,
required Color borderColor,
required List<Color> gradientColors,
required String imageAsset,
required double imageTop,
required double imageEnd,
required List<String> avatars,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 60.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14.w),
border: Border.all(color: borderColor, width: 1.w),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: gradientColors,
),
),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
PositionedDirectional(
end: imageEnd.w,
top: imageTop.w,
width: 66.w,
height: 66.w,
child: Image.asset(imageAsset, fit: BoxFit.contain),
),
_buildLeaderboardTitle(title, borderColor),
PositionedDirectional(
start: 10.w,
bottom: 6.w,
child: Row(
children: List.generate(avatars.length, (index) {
const frameAssets = [
_rankFrameSilverAsset,
_rankFrameGoldAsset,
_rankFrameBronzeAsset,
];
return Padding(
padding: EdgeInsetsDirectional.only(
end: index == avatars.length - 1 ? 0 : 3.2.w,
),
child: _buildLeaderboardAvatar(
avatar: avatars[index],
size: 24.w,
avatarSize: 22.w,
frameAsset: frameAssets[index],
),
);
}),
),
),
],
),
),
);
}
Widget _buildLeaderboardAvatar({
required String avatar,
required double size,
required double avatarSize,
required String frameAsset,
}) {
final url = avatar.trim();
final avatarWidget =
url.isEmpty
? ClipOval(
child: Image.asset(
'sc_images/general/sc_icon_avar_defalt.png',
width: avatarSize,
height: avatarSize,
fit: BoxFit.cover,
),
)
: head(
url: url,
width: avatarSize,
height: avatarSize,
fit: BoxFit.cover,
gifFit: BoxFit.cover,
);
return SizedBox(
width: size,
height: size,
child: Stack(
alignment: Alignment.center,
children: [
avatarWidget,
Positioned.fill(
child: IgnorePointer(
child: Image.asset(frameAsset, fit: BoxFit.contain),
),
),
],
),
);
}
Widget _buildLeaderboardTitle(String title, Color color) {
return PositionedDirectional(
start: 0,
top: 0,
child: Container(
height: 20.w,
constraints: BoxConstraints(minWidth: 70.w, maxWidth: 90.w),
padding: EdgeInsetsDirectional.only(start: 10.w, end: 14.w),
alignment: AlignmentDirectional.centerStart,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadiusDirectional.only(
bottomEnd: Radius.circular(8.w),
),
),
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(0xFFDEFFF7),
fontSize: 14.sp,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.w800,
height: 1,
),
),
),
);
}
Widget _buildRoomsSection() {
if (!_hasCompletedInitialRoomLoad && isLoading && rooms.isEmpty) {
return _buildRoomGridSkeleton();
}
if (rooms.isEmpty) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
loadData();
},
child: mainEmpty(msg: SCAppLocalizations.of(context)!.noData),
);
}
final countryOptions = _roomCountryFilterOptions();
final visibleRooms = _filteredRooms();
final countrySelector = _buildCountryFilterSelector(countryOptions);
if (visibleRooms.isEmpty) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
countrySelector,
Padding(
padding: EdgeInsets.only(top: 16.w),
child: mainEmpty(msg: SCAppLocalizations.of(context)!.noData),
),
],
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
countrySelector,
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 172 / 200,
mainAxisSpacing: 8.w,
crossAxisSpacing: 10.w,
),
padding: EdgeInsets.fromLTRB(10.w, 8.w, 10.w, 10.w),
itemCount: visibleRooms.length,
itemBuilder: (context, index) {
return _buildItem(visibleRooms[index], index);
},
),
],
);
}
Widget _buildCountryFilterSelector(
List<_RoomCountryFilterOption> countryOptions,
) {
if (countryOptions.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: EdgeInsetsDirectional.fromSTEB(0, 10.w, 0, 0),
child: SizedBox(
height: 28.w,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
padding: EdgeInsetsDirectional.symmetric(horizontal: 10.w),
itemCount: countryOptions.length + 1,
separatorBuilder: (context, index) => SizedBox(width: 8.w),
itemBuilder: (context, index) {
if (index == 0) {
return _buildCountryFilterChip(
label: SCAppLocalizations.of(context)!.all,
);
}
final option = countryOptions[index - 1];
return Consumer<SCAppGeneralManager>(
builder: (context, provider, child) {
final country = provider.findCountryByName(option.name);
final countryFlagUrl =
_trimToNull(option.flagUrl) ??
_trimToNull(country?.nationalFlag);
final countryLabel =
_trimToNull(country?.aliasName) ??
_trimToNull(country?.countryName) ??
option.name;
return _buildCountryFilterChip(
label: countryLabel,
countryKey: option.key,
flagUrl: countryFlagUrl,
);
},
);
},
),
),
);
}
Widget _buildCountryFilterChip({
required String label,
String? countryKey,
String? flagUrl,
}) {
final isSelected = _selectedCountryKey == countryKey;
final resolvedFlagUrl = _trimToNull(flagUrl);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_selectedCountryKey == countryKey) {
return;
}
setState(() {
_selectedCountryKey = countryKey;
});
unawaited(_syncVisibleRoomMemberCounts(force: true));
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOut,
height: 28.w,
constraints: BoxConstraints(minWidth: countryKey == null ? 63.w : 0),
alignment: AlignmentDirectional.center,
padding: EdgeInsets.symmetric(horizontal: 14.w),
decoration: BoxDecoration(
color:
isSelected
? Colors.transparent
: const Color(0xFF06392C).withValues(alpha: 0.86),
borderRadius: BorderRadius.circular(999.w),
border: Border.all(
color: isSelected ? _kPartyAccent : Colors.transparent,
width: 1.w,
),
boxShadow:
isSelected
? [
BoxShadow(
color: _kPartyAccent.withValues(alpha: 0.18),
blurRadius: 6.w,
),
]
: null,
),
child: Directionality(
textDirection: TextDirection.ltr,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (resolvedFlagUrl != null) ...[
netImage(
url: resolvedFlagUrl,
width: 15.w,
height: 10.w,
borderRadius: BorderRadius.circular(2.w),
),
SizedBox(width: 4.w),
],
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
textDirection: Directionality.of(context),
style: TextStyle(
color: _kPartyAccent,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
height: 1,
),
),
),
],
),
),
),
);
}
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({bool forceRefresh = false}) {
final generalManager = Provider.of<SCAppGeneralManager>(
context,
listen: false,
);
setState(() {
isLoading = true;
_isBannerLoading =
generalManager.exploreBanners.isEmpty &&
generalManager.homeBanners.isEmpty;
_isLeaderboardLoading = generalManager.appLeaderResult == null;
});
SCHomeRoomPreloadManager.instance
.loadPartyRooms(forceRefresh: forceRefresh)
.then((values) {
rooms = _mergeLatestRoomsWithCurrentCounters(values);
_syncCountryFilterWithRooms();
isLoading = false;
_hasCompletedInitialRoomLoad = true;
_refreshController.refreshCompleted();
_refreshController.loadComplete();
if (mounted) {
setState(() {});
unawaited(_syncVisibleRoomMemberCounts(force: true));
}
})
.catchError((e) {
_refreshController.loadNoData();
_refreshController.refreshCompleted();
isLoading = false;
_hasCompletedInitialRoomLoad = true;
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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 1,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF222222),
borderRadius: BorderRadius.circular(12.w),
border: Border.all(color: _kPartyAccent.withAlpha(51)),
),
clipBehavior: Clip.antiAlias,
child: ClipRRect(
borderRadius: BorderRadius.circular(12.w),
child: Stack(
children: [
Positioned.fill(
child: netImage(
url: resolveRoomCoverUrl(res.id, res.roomCover),
defaultImg: kRoomCoverDefaultImg,
fit: BoxFit.cover,
),
),
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.w),
border: Border.all(
color: _kPartyAccent.withValues(alpha: 0.2),
width: 1.w,
),
),
),
),
PositionedDirectional(
start: 8.w,
bottom: 9.w,
child:
res.extValues?.existsPassword != true
? Row(
children: [
SCRoomLiveAudioIndicator(
width: 14.w,
height: 14.w,
),
SizedBox(width: 3.w),
text(
res.displayMemberCount,
fontSize: 10.sp,
lineHeight: 1,
),
],
)
: Image.asset(
"sc_images/index/sc_icon_room_suo.png",
width: 20.w,
height: 20.w,
),
),
],
),
),
),
),
SizedBox(height: 6.w),
Row(
children: [
Consumer<SCAppGeneralManager>(
builder: (_, provider, __) {
final flagUrl =
_trimToNull(res.nationalFlag) ??
_trimToNull(
provider
.findCountryByName(res.countryName ?? "")
?.nationalFlag,
);
if (flagUrl == null) {
return const SizedBox.shrink();
}
return Padding(
padding: EdgeInsetsDirectional.only(end: 4.w),
child: netImage(
url: flagUrl,
width: 22.w,
height: 16.w,
borderRadius: BorderRadius.circular(3.w),
),
);
},
),
Expanded(
child: Text(
(res.roomName ?? "").toUpperCase(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w700,
height: 16 / 14,
),
),
),
],
),
],
),
onTap: () {
Provider.of<RtcProvider>(context, listen: false).joinVoiceRoomSession(
context,
res.id ?? "",
previewData: RoomEntryPreviewData.fromSocialChatRoom(res),
);
},
);
}
}