diff --git a/lib/main.dart b/lib/main.dart index a4ee1e2..1e244e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,6 +34,7 @@ import 'services/auth/authentication_manager.dart'; import 'services/gift/gift_animation_manager.dart'; import 'services/gift/gift_system_manager.dart'; import 'services/payment/google_payment_manager.dart'; +import 'services/payment/mifa_pay_manager.dart'; import 'services/localization/localization_manager.dart'; import 'services/room/rc_room_manager.dart'; import 'services/audio/rtc_manager.dart'; @@ -261,6 +262,10 @@ class RootAppWithProviders extends StatelessWidget { lazy: true, create: (context) => IOSPaymentProcessor(), ), + ChangeNotifierProvider( + lazy: true, + create: (context) => MiFaPayManager(), + ), ChangeNotifierProvider( lazy: true, create: (context) => ShopManager(), @@ -408,8 +413,17 @@ class _YumiApplicationState extends State { children: [ child ?? const SizedBox.shrink(), if (SCGlobalConfig.allowsHighCostAnimations) - const Positioned.fill( - child: VapPlusSvgaPlayer(tag: "room_gift"), + Consumer( + builder: (context, rtcProvider, _) { + if (!rtcProvider.roomVisualEffectsEnabled) { + return const SizedBox.shrink(); + } + return const Positioned.fill( + child: VapPlusSvgaPlayer( + tag: "room_gift", + ), + ); + }, ), Positioned.fill( child: RoomGiftSeatFlightOverlay( diff --git a/lib/modules/admin/editing/sc_editing_user_room_page.dart b/lib/modules/admin/editing/sc_editing_user_room_page.dart index faa4149..6f7e30f 100644 --- a/lib/modules/admin/editing/sc_editing_user_room_page.dart +++ b/lib/modules/admin/editing/sc_editing_user_room_page.dart @@ -55,7 +55,9 @@ class _SCEditingUserRoomPageState extends State { @override void initState() { super.initState(); - final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(widget.type); + final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping( + widget.type, + ); if (violationTypeMapping.isNotEmpty) { // 获取第一个值作为默认违规类型 violationType = violationTypeMapping.values.first; @@ -65,6 +67,12 @@ class _SCEditingUserRoomPageState extends State { imageUrls = List.generate(maxImageCount, (index) => ''); } + @override + void dispose() { + _descriptionController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Stack( @@ -166,8 +174,14 @@ class _SCEditingUserRoomPageState extends State { (widget.userProfile ?.hasSpecialId() ?? false) - ? _strategy.getAdminEditingIcon('specialIdBg') - : _strategy.getAdminEditingIcon('normalIdBg'), + ? _strategy + .getAdminEditingIcon( + 'specialIdBg', + ) + : _strategy + .getAdminEditingIcon( + 'normalIdBg', + ), ), fit: BoxFit.fitWidth, ), @@ -185,7 +199,9 @@ class _SCEditingUserRoomPageState extends State { ), SizedBox(width: 5.w), Image.asset( - _strategy.getAdminEditingIcon('copyId'), + _strategy.getAdminEditingIcon( + 'copyId', + ), width: 12.w, height: 12.w, ), @@ -281,7 +297,9 @@ class _SCEditingUserRoomPageState extends State { ), SizedBox(width: 5.w), Image.asset( - _strategy.getAdminEditingIcon('copyId'), + _strategy.getAdminEditingIcon( + 'copyId', + ), width: 12.w, height: 12.w, color: Colors.black26, @@ -343,7 +361,7 @@ class _SCEditingUserRoomPageState extends State { children: [ // 动态生成违规类型选项 ..._buildViolationTypeOptions(context), - SizedBox(height: 20.w,), + SizedBox(height: 20.w), Row( children: [ text( @@ -358,11 +376,13 @@ class _SCEditingUserRoomPageState extends State { SizedBox(height: 8.w), Container( padding: EdgeInsets.all(5.w), - decoration: _strategy.getAdminEditingInputDecoration(), + decoration: + _strategy.getAdminEditingInputDecoration(), child: TextField( controller: _descriptionController, onChanged: (text) {}, - maxLength: _strategy.getAdminEditingDescriptionMaxLength(), + maxLength: + _strategy.getAdminEditingDescriptionMaxLength(), maxLines: 5, decoration: InputDecoration( hintText: @@ -413,52 +433,75 @@ class _SCEditingUserRoomPageState extends State { Row( children: [ // 动态生成图片上传组件 - ...List.generate(_strategy.getAdminEditingMaxImageUploadCount(), (index) { - return Expanded( - child: GestureDetector( - child: Stack( - children: [ - imageUrls[index].isNotEmpty - ? netImage(url: imageUrls[index], height: 100.w) - : Image.asset( - _strategy.getAdminEditingIcon('addPic'), - height: 100.w, - ), - imageUrls[index].isNotEmpty - ? Positioned( - top: 5.w, - right: 5.w, - child: GestureDetector( - child: Image.asset( - _strategy.getAdminEditingIcon('closePic'), - width: 14.w, - height: 14.w, - ), - onTap: () { - setState(() { - imageUrls[index] = ""; - }); - }, - ), - ) - : Container(), - ], - ), - onTap: () { - SCPickUtils.pickImage(context, ( - bool success, - String url, - ) { - if (success) { - setState(() { - imageUrls[index] = url; - }); - } - }); + ...List.generate( + _strategy + .getAdminEditingMaxImageUploadCount(), + (index) { + return Expanded( + child: GestureDetector( + child: Stack( + children: [ + imageUrls[index].isNotEmpty + ? netImage( + url: imageUrls[index], + height: 100.w, + ) + : Image.asset( + _strategy.getAdminEditingIcon( + 'addPic', + ), + height: 100.w, + ), + imageUrls[index].isNotEmpty + ? Positioned( + top: 5.w, + right: 5.w, + child: GestureDetector( + child: Image.asset( + _strategy + .getAdminEditingIcon( + 'closePic', + ), + width: 14.w, + height: 14.w, + ), + onTap: () { + setState(() { + imageUrls[index] = ""; + }); + }, + ), + ) + : Container(), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + setState(() { + imageUrls[index] = url; + }); + } + }); + }, + ), + ); }, - ), - ); - }).expand((widget) => [widget, if (_strategy.getAdminEditingMaxImageUploadCount() > 1 && widget != Expanded) SizedBox(width: 5.w)]).toList(), + ) + .expand( + (widget) => [ + widget, + if (_strategy + .getAdminEditingMaxImageUploadCount() > + 1 && + widget != Expanded) + SizedBox(width: 5.w), + ], + ) + .toList(), ], ), SizedBox(height: 15.w), @@ -472,33 +515,44 @@ class _SCEditingUserRoomPageState extends State { debouncer.debounce( duration: Duration(milliseconds: 350), onDebounce: () { - List uploadedImages = imageUrls.where((url) => url.isNotEmpty).toList(); + List uploadedImages = + imageUrls.where((url) => url.isNotEmpty).toList(); if (widget.type == "User") { - SCAccountRepository().userViolationHandle( - widget.userProfile?.id ?? "", - violationType, - 1, - _descriptionController.text, - imageUrls: imageUrls, - ).then((b){ - SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); - SCNavigatorUtils.goBack(context); - }).catchError((e){ - - }); + SCAccountRepository() + .userViolationHandle( + widget.userProfile?.id ?? "", + violationType, + 1, + _descriptionController.text, + imageUrls: imageUrls, + ) + .then((b) { + SCTts.show( + SCAppLocalizations.of( + context, + )!.operationSuccessful, + ); + SCNavigatorUtils.goBack(context); + }) + .catchError((e) {}); } else { - SCChatRoomRepository().roomViolationHandle( - widget.roomProfile?.id ?? "", - violationType, - 1, - _descriptionController.text, - imageUrls: imageUrls, - ).then((b){ - SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); - SCNavigatorUtils.goBack(context); - }).catchError((e){ - - }); + SCChatRoomRepository() + .roomViolationHandle( + widget.roomProfile?.id ?? "", + violationType, + 1, + _descriptionController.text, + imageUrls: imageUrls, + ) + .then((b) { + SCTts.show( + SCAppLocalizations.of( + context, + )!.operationSuccessful, + ); + SCNavigatorUtils.goBack(context); + }) + .catchError((e) {}); } }, ); @@ -512,7 +566,9 @@ class _SCEditingUserRoomPageState extends State { gradient: LinearGradient( begin: AlignmentDirectional.topCenter, end: AlignmentDirectional.bottomCenter, - colors: _strategy.getAdminEditingButtonGradient('warning'), + colors: _strategy.getAdminEditingButtonGradient( + 'warning', + ), ), borderRadius: BorderRadius.circular(25.w), ), @@ -533,33 +589,44 @@ class _SCEditingUserRoomPageState extends State { debouncer.debounce( duration: Duration(milliseconds: 350), onDebounce: () { - List uploadedImages = imageUrls.where((url) => url.isNotEmpty).toList(); + List uploadedImages = + imageUrls.where((url) => url.isNotEmpty).toList(); if (widget.type == "User") { - SCAccountRepository().userViolationHandle( - widget.userProfile?.id ?? "", - violationType, - 2, - _descriptionController.text, - imageUrls: imageUrls, - ).then((b){ - SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); - SCNavigatorUtils.goBack(context); - }).catchError((e){ - - }); + SCAccountRepository() + .userViolationHandle( + widget.userProfile?.id ?? "", + violationType, + 2, + _descriptionController.text, + imageUrls: imageUrls, + ) + .then((b) { + SCTts.show( + SCAppLocalizations.of( + context, + )!.operationSuccessful, + ); + SCNavigatorUtils.goBack(context); + }) + .catchError((e) {}); } else { - SCChatRoomRepository().roomViolationHandle( - widget.roomProfile?.id ?? "", - violationType, - 2, - _descriptionController.text, - imageUrls: imageUrls, - ).then((b){ - SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); - SCNavigatorUtils.goBack(context); - }).catchError((e){ - - }); + SCChatRoomRepository() + .roomViolationHandle( + widget.roomProfile?.id ?? "", + violationType, + 2, + _descriptionController.text, + imageUrls: imageUrls, + ) + .then((b) { + SCTts.show( + SCAppLocalizations.of( + context, + )!.operationSuccessful, + ); + SCNavigatorUtils.goBack(context); + }) + .catchError((e) {}); } }, ); @@ -573,7 +640,9 @@ class _SCEditingUserRoomPageState extends State { gradient: LinearGradient( begin: AlignmentDirectional.topCenter, end: AlignmentDirectional.bottomCenter, - colors: _strategy.getAdminEditingButtonGradient('adjust'), + colors: _strategy.getAdminEditingButtonGradient( + 'adjust', + ), ), borderRadius: BorderRadius.circular(25.w), ), @@ -598,17 +667,23 @@ class _SCEditingUserRoomPageState extends State { } List _buildViolationTypeOptions(BuildContext context) { - final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(widget.type); + final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping( + widget.type, + ); final List options = []; if (violationTypeMapping.isEmpty) { // 如果没有映射,返回默认选项 if (widget.type == "User") { options.add(_item(SCAppLocalizations.of(context)!.userName, 0, 1)); - options.add(_item(SCAppLocalizations.of(context)!.userProfilePicture, 1, 2)); + options.add( + _item(SCAppLocalizations.of(context)!.userProfilePicture, 1, 2), + ); } else { options.add(_item(SCAppLocalizations.of(context)!.roomName, 0, 3)); - options.add(_item(SCAppLocalizations.of(context)!.roomProfilePicture, 1, 4)); + options.add( + _item(SCAppLocalizations.of(context)!.roomProfilePicture, 1, 4), + ); options.add(_item(SCAppLocalizations.of(context)!.roomNotice, 2, 5)); options.add(_item(SCAppLocalizations.of(context)!.roomTheme, 3, 6)); } @@ -677,7 +752,12 @@ class _SCEditingUserRoomPageState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - text(str, fontSize: 13.sp, textColor: Colors.black,fontWeight: FontWeight.w500), + text( + str, + fontSize: 13.sp, + textColor: Colors.black, + fontWeight: FontWeight.w500, + ), Spacer(), selectedIndex == index ? Image.asset( diff --git a/lib/modules/admin/search/sc_edit_room_search_admin_page.dart b/lib/modules/admin/search/sc_edit_room_search_admin_page.dart index ae1fc93..4c400da 100644 --- a/lib/modules/admin/search/sc_edit_room_search_admin_page.dart +++ b/lib/modules/admin/search/sc_edit_room_search_admin_page.dart @@ -40,6 +40,12 @@ class _SCEditRoomSearchAdminPageState extends State super.initState(); } + @override + void dispose() { + _textEditingController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Stack( @@ -79,17 +85,25 @@ class _SCEditRoomSearchAdminPageState extends State child: searchWidget( hint: SCAppLocalizations.of(context)!.enterTheRoomId, controller: _textEditingController, - borderColor: _strategy.getAdminSearchInputBorderColor('roomSearch'), - textColor: _strategy.getAdminSearchInputTextColor('roomSearch'), + borderColor: _strategy.getAdminSearchInputBorderColor( + 'roomSearch', + ), + textColor: _strategy.getAdminSearchInputTextColor( + 'roomSearch', + ), ), ), socialchatGradientButton( text: SCAppLocalizations.of(context)!.search, radius: 25, textSize: 14.sp, - textColor: _strategy.getAdminSearchButtonTextColor('roomSearch'), + textColor: _strategy.getAdminSearchButtonTextColor( + 'roomSearch', + ), gradient: LinearGradient( - colors: _strategy.getAdminSearchButtonGradient('roomSearch'), + colors: _strategy.getAdminSearchButtonGradient( + 'roomSearch', + ), begin: Alignment.centerLeft, end: Alignment.centerRight, ), diff --git a/lib/modules/admin/search/sc_edit_user_search_admin_page.dart b/lib/modules/admin/search/sc_edit_user_search_admin_page.dart index d8b14cc..bad7ec5 100644 --- a/lib/modules/admin/search/sc_edit_user_search_admin_page.dart +++ b/lib/modules/admin/search/sc_edit_user_search_admin_page.dart @@ -42,6 +42,12 @@ class _SCEditUserSearchAdminPageState extends State super.initState(); } + @override + void dispose() { + _textEditingController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Stack( @@ -81,17 +87,25 @@ class _SCEditUserSearchAdminPageState extends State child: searchWidget( hint: SCAppLocalizations.of(context)!.enterTheUserId, controller: _textEditingController, - borderColor: _strategy.getAdminSearchInputBorderColor('userSearch'), - textColor: _strategy.getAdminSearchInputTextColor('userSearch'), + borderColor: _strategy.getAdminSearchInputBorderColor( + 'userSearch', + ), + textColor: _strategy.getAdminSearchInputTextColor( + 'userSearch', + ), ), ), socialchatGradientButton( text: SCAppLocalizations.of(context)!.search, radius: 25, textSize: 14.sp, - textColor: _strategy.getAdminSearchButtonTextColor('userSearch'), + textColor: _strategy.getAdminSearchButtonTextColor( + 'userSearch', + ), gradient: LinearGradient( - colors: _strategy.getAdminSearchButtonGradient('userSearch'), + colors: _strategy.getAdminSearchButtonGradient( + 'userSearch', + ), begin: Alignment.centerLeft, end: Alignment.centerRight, ), @@ -190,8 +204,12 @@ class _SCEditUserSearchAdminPageState extends State image: DecorationImage( image: AssetImage( (data.hasSpecialId() ?? false) - ? _strategy.getAdminSearchUserInfoIcon('specialIdBg') - : _strategy.getAdminSearchUserInfoIcon('normalIdBg'), + ? _strategy.getAdminSearchUserInfoIcon( + 'specialIdBg', + ) + : _strategy.getAdminSearchUserInfoIcon( + 'normalIdBg', + ), ), fit: BoxFit.fitWidth, ), diff --git a/lib/modules/auth/account/sc_login_with_account_page.dart b/lib/modules/auth/account/sc_login_with_account_page.dart index c668d53..742fec2 100644 --- a/lib/modules/auth/account/sc_login_with_account_page.dart +++ b/lib/modules/auth/account/sc_login_with_account_page.dart @@ -40,28 +40,35 @@ class SCLoginWithAccountPageState extends State ///密码控制器 TextEditingController passController = TextEditingController(); - @override - void initState() { - super.initState(); - String account = DataPersistence.getString("Login_Account"); - String pwd = DataPersistence.getString("Login_Pwd"); - // String account = DataPersistence.getString( - // "Login_Account", - // defaultValue: "123456", - // ); - // String pwd = DataPersistence.getString( - // "Login_Pwd", - // defaultValue: "123456", - // ); - // if (account.isEmpty) { - // account = "123456"; - // } - // if (pwd.isEmpty) { - // pwd = "123456"; - // } - accountController.text = account; - passController.text = pwd; - } + @override + void initState() { + super.initState(); + String account = DataPersistence.getString("Login_Account"); + String pwd = DataPersistence.getString("Login_Pwd"); + // String account = DataPersistence.getString( + // "Login_Account", + // defaultValue: "123456", + // ); + // String pwd = DataPersistence.getString( + // "Login_Pwd", + // defaultValue: "123456", + // ); + // if (account.isEmpty) { + // account = "123456"; + // } + // if (pwd.isEmpty) { + // pwd = "123456"; + // } + accountController.text = account; + passController.text = pwd; + } + + @override + void dispose() { + accountController.dispose(); + passController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { diff --git a/lib/modules/chat/message_chat_page.dart b/lib/modules/chat/message_chat_page.dart index 2fc38e1..9608ee2 100644 --- a/lib/modules/chat/message_chat_page.dart +++ b/lib/modules/chat/message_chat_page.dart @@ -138,6 +138,10 @@ class _SCMessageChatPageState extends State { rtmProvider?.onMessageRecvC2CReadListener = null; rtmProvider?.onRevokeMessageListener = null; rtmProvider?.onNewMessageCurrentConversationListener = null; + _textController.dispose(); + _scrollController.dispose(); + _refreshController.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -258,10 +262,10 @@ class _SCMessageChatPageState extends State { top: false, child: Column( children: [ - if (!SCGlobalConfig.isSystemConversationId( - currentConversation?.conversationID, - )) - _input(), + if (!SCGlobalConfig.isSystemConversationId( + currentConversation?.conversationID, + )) + _input(), _tools(provider), _emoji(), // _fahongbao(), @@ -280,16 +284,18 @@ class _SCMessageChatPageState extends State { ); } - void loadFriend() { - if (SCGlobalConfig.isSystemConversationId(currentConversation?.conversationID) || - SCGlobalConfig.isSystemUserId(currentConversation?.userID)) { - return; - } - SCLoadingManager.show(); - Future.wait([ - SCAccountRepository().loadUserInfo("${currentConversation?.userID}"), - SCAccountRepository().friendRelationCheck( - "${currentConversation?.userID}", + void loadFriend() { + if (SCGlobalConfig.isSystemConversationId( + currentConversation?.conversationID, + ) || + SCGlobalConfig.isSystemUserId(currentConversation?.userID)) { + return; + } + SCLoadingManager.show(); + Future.wait([ + SCAccountRepository().loadUserInfo("${currentConversation?.userID}"), + SCAccountRepository().friendRelationCheck( + "${currentConversation?.userID}", ), ]) .then((result) { diff --git a/lib/modules/home/popular/event/home_event_page.dart b/lib/modules/home/popular/event/home_event_page.dart index 915a5e3..06080c9 100644 --- a/lib/modules/home/popular/event/home_event_page.dart +++ b/lib/modules/home/popular/event/home_event_page.dart @@ -30,6 +30,12 @@ class _HomeEventPageState extends State _loadData(); } + @override + void dispose() { + _refreshController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -50,7 +56,7 @@ class _HomeEventPageState extends State ), ], ), - SizedBox(height: 8.w,), + SizedBox(height: 8.w), Row( children: [ SizedBox(width: 20.w), diff --git a/lib/modules/home/popular/follow/sc_room_follow_page.dart b/lib/modules/home/popular/follow/sc_room_follow_page.dart index 0c9a3a7..5173213 100644 --- a/lib/modules/home/popular/follow/sc_room_follow_page.dart +++ b/lib/modules/home/popular/follow/sc_room_follow_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app_localizations.dart'; @@ -22,17 +24,49 @@ class SCRoomFollowPage extends SCPageList { } class _RoomFollowPageState - extends SCPageListState { + extends SCPageListState + with WidgetsBindingObserver { + static const Duration _roomListRefreshInterval = Duration(seconds: 15); + String? lastId; + Timer? _roomListRefreshTimer; + bool _isSilentRefreshingRooms = false; + bool _wasTickerModeEnabled = false; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); enablePullUp = true; backgroundColor = Colors.transparent; isGridView = true; gridViewCount = 2; loadData(1); + _startRoomListAutoRefresh(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _roomListRefreshTimer?.cancel(); + 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 @@ -194,7 +228,7 @@ class _RoomFollowPageState ?.isEmpty ?? false) ? text( - roomRes.roomProfile?.extValues?.memberQuantity ?? "0", + roomRes.roomProfile?.displayMemberCount ?? "0", fontSize: 10.sp, lineHeight: 1, ) @@ -295,4 +329,85 @@ class _RoomFollowPageState } } } + + void _startRoomListAutoRefresh() { + _roomListRefreshTimer?.cancel(); + _roomListRefreshTimer = Timer.periodic(_roomListRefreshInterval, (_) { + _refreshRoomsSilently(); + }); + } + + bool _canRefreshRoomListSilently() { + return mounted && + TickerMode.valuesOf(context).enabled && + !_isSilentRefreshingRooms && + !isLoading; + } + + bool _sameRoom(FollowRoomRes previous, FollowRoomRes next) { + return previous.roomProfile?.id == next.roomProfile?.id && + previous.roomProfile?.roomName == next.roomProfile?.roomName && + previous.roomProfile?.roomCover == next.roomProfile?.roomCover && + previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon && + previous.roomProfile?.displayMemberCount == + next.roomProfile?.displayMemberCount && + previous.roomProfile?.extValues?.roomSetting?.password == + next.roomProfile?.extValues?.roomSetting?.password; + } + + bool _sameRoomLists(List previous, List 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 _applyLatestRooms(List latestRooms) { + if (items.isEmpty || currentPage <= 1) { + if (_sameRoomLists(items, latestRooms)) { + return false; + } + items + ..clear() + ..addAll(latestRooms); + lastId = latestRooms.isNotEmpty ? latestRooms.last.id : null; + return true; + } + + bool changed = false; + final replaceCount = latestRooms.length < items.length + ? latestRooms.length + : items.length; + for (int i = 0; i < replaceCount; i++) { + if (!_sameRoom(items[i], latestRooms[i])) { + items[i] = latestRooms[i]; + changed = true; + } + } + return changed; + } + + Future _refreshRoomsSilently() async { + if (!_canRefreshRoomListSilently()) { + return; + } + _isSilentRefreshingRooms = true; + try { + final latestRooms = await SCAccountRepository().followRoomList(); + if (!mounted || !TickerMode.valuesOf(context).enabled) { + return; + } + if (_applyLatestRooms(latestRooms)) { + setState(() {}); + } + } catch (_) { + } finally { + _isSilentRefreshingRooms = false; + } + } } diff --git a/lib/modules/home/popular/history/sc_room_history_page.dart b/lib/modules/home/popular/history/sc_room_history_page.dart index ce86049..907678b 100644 --- a/lib/modules/home/popular/history/sc_room_history_page.dart +++ b/lib/modules/home/popular/history/sc_room_history_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app_localizations.dart'; @@ -22,17 +24,49 @@ class SCRoomHistoryPage extends SCPageList { } class _SCRoomHistoryPageState - extends SCPageListState { + extends SCPageListState + with WidgetsBindingObserver { + static const Duration _roomListRefreshInterval = Duration(seconds: 15); + String? lastId; + Timer? _roomListRefreshTimer; + bool _isSilentRefreshingRooms = false; + bool _wasTickerModeEnabled = false; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); enablePullUp = true; backgroundColor = Colors.transparent; isGridView = true; gridViewCount = 2; loadData(1); + _startRoomListAutoRefresh(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _roomListRefreshTimer?.cancel(); + 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 @@ -195,7 +229,7 @@ class _SCRoomHistoryPageState ?.isEmpty ?? false) ? text( - roomRes.roomProfile?.extValues?.memberQuantity ?? "0", + roomRes.roomProfile?.displayMemberCount ?? "0", fontSize: 10.sp, lineHeight: 1, ) @@ -295,4 +329,85 @@ class _SCRoomHistoryPageState } } } + + void _startRoomListAutoRefresh() { + _roomListRefreshTimer?.cancel(); + _roomListRefreshTimer = Timer.periodic(_roomListRefreshInterval, (_) { + _refreshRoomsSilently(); + }); + } + + bool _canRefreshRoomListSilently() { + return mounted && + TickerMode.valuesOf(context).enabled && + !_isSilentRefreshingRooms && + !isLoading; + } + + bool _sameRoom(FollowRoomRes previous, FollowRoomRes next) { + return previous.roomProfile?.id == next.roomProfile?.id && + previous.roomProfile?.roomName == next.roomProfile?.roomName && + previous.roomProfile?.roomCover == next.roomProfile?.roomCover && + previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon && + previous.roomProfile?.displayMemberCount == + next.roomProfile?.displayMemberCount && + previous.roomProfile?.extValues?.roomSetting?.password == + next.roomProfile?.extValues?.roomSetting?.password; + } + + bool _sameRoomLists(List previous, List 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 _applyLatestRooms(List latestRooms) { + if (items.isEmpty || currentPage <= 1) { + if (_sameRoomLists(items, latestRooms)) { + return false; + } + items + ..clear() + ..addAll(latestRooms); + lastId = latestRooms.isNotEmpty ? latestRooms.last.id : null; + return true; + } + + bool changed = false; + final replaceCount = latestRooms.length < items.length + ? latestRooms.length + : items.length; + for (int i = 0; i < replaceCount; i++) { + if (!_sameRoom(items[i], latestRooms[i])) { + items[i] = latestRooms[i]; + changed = true; + } + } + return changed; + } + + Future _refreshRoomsSilently() async { + if (!_canRefreshRoomListSilently()) { + return; + } + _isSilentRefreshingRooms = true; + try { + final latestRooms = await SCAccountRepository().trace(); + if (!mounted || !TickerMode.valuesOf(context).enabled) { + return; + } + if (_applyLatestRooms(latestRooms)) { + setState(() {}); + } + } catch (_) { + } finally { + _isSilentRefreshingRooms = false; + } + } } diff --git a/lib/modules/home/popular/mine/sc_home_mine_page.dart b/lib/modules/home/popular/mine/sc_home_mine_page.dart index 5e48918..1e7d3ff 100644 --- a/lib/modules/home/popular/mine/sc_home_mine_page.dart +++ b/lib/modules/home/popular/mine/sc_home_mine_page.dart @@ -85,17 +85,18 @@ class _HomeMinePageState extends State labelPadding: EdgeInsets.symmetric(horizontal: 12.w), labelColor: SocialChatTheme.primaryLight, isScrollable: true, + splashFactory: NoSplash.splashFactory, + overlayColor: WidgetStateProperty.all(Colors.transparent), indicator: BoxDecoration(), unselectedLabelColor: Colors.white, labelStyle: TextStyle( - fontSize: 15.sp, - fontFamily: 'MyCustomFont', - fontWeight: FontWeight.w600, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + fontSize: 19.sp, ), unselectedLabelStyle: TextStyle( - fontSize: 13.sp, - fontFamily: 'MyCustomFont', - fontWeight: FontWeight.w500, + fontWeight: FontWeight.normal, + fontSize: 14.sp, ), indicatorColor: Colors.transparent, dividerColor: Colors.transparent, diff --git a/lib/modules/home/popular/party/sc_home_party_page.dart b/lib/modules/home/popular/party/sc_home_party_page.dart index b04fbb9..872aadf 100644 --- a/lib/modules/home/popular/party/sc_home_party_page.dart +++ b/lib/modules/home/popular/party/sc_home_party_page.dart @@ -1,7 +1,8 @@ -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:carousel_slider/carousel_slider.dart'; +import 'dart:async'; +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'; @@ -12,12 +13,13 @@ 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'; +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 '../../../../ui_kit/widgets/svga/sc_svga_asset_widget.dart'; +import '../../../index/main_route.dart'; const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450); const Color _kPartySkeletonShell = Color(0xFF0F3730); @@ -33,36 +35,71 @@ class SCHomePartyPage extends StatefulWidget { State createState() => _HomePartyPageState(); } -class _HomePartyPageState extends State - with SingleTickerProviderStateMixin { - List historyRooms = []; - String? lastId; - bool isLoading = false; - bool _isBannerLoading = false; - bool _isLeaderboardLoading = false; +class _HomePartyPageState extends State + with SingleTickerProviderStateMixin, WidgetsBindingObserver { + static const Duration _roomListRefreshInterval = Duration(seconds: 15); + static const Duration _roomCounterHydrationMinGap = Duration(seconds: 20); + static const int _roomCounterHydrationLimit = 6; + static const List _topRankBorderAssets = [ + "sc_images/index/sc_icon_home_room_rank_border_1.svga", + "sc_images/index/sc_icon_home_room_rank_border_2.svga", + "sc_images/index/sc_icon_home_room_rank_border_3.svga", + ]; + + 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(); - } + ); + late final AnimationController _skeletonController; + List rooms = []; + int _currentIndex = 0; + Timer? _roomListRefreshTimer; + bool _isSilentRefreshingRooms = false; + bool _isHydratingVisibleRoomCounters = false; + bool _wasTickerModeEnabled = false; + DateTime? _lastRoomCounterHydrationAt; + + @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) { @@ -119,7 +156,171 @@ class _HomePartyPageState extends State ], ), ); - } + } + + void _startRoomListAutoRefresh() { + _roomListRefreshTimer?.cancel(); + _roomListRefreshTimer = Timer.periodic(_roomListRefreshInterval, (_) { + _refreshRoomsSilently(); + }); + } + + bool _canRefreshRoomListSilently() { + return mounted && + TickerMode.valuesOf(context).enabled && + !_isSilentRefreshingRooms && + !isLoading; + } + + bool _sameRooms( + List previous, + List 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.roomName == next.roomName && + previous.roomCover == next.roomCover && + previous.roomGameIcon == next.roomGameIcon && + previous.displayMemberCount == next.displayMemberCount && + previous.extValues?.roomSetting?.password == + next.extValues?.roomSetting?.password; + } + + List _mergeLatestRoomsWithCurrentCounters( + List 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(); + } + + Future _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 = + rooms + .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(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.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 _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; + }); + unawaited(_syncVisibleRoomMemberCounts(force: true)); + } catch (_) { + } finally { + _isSilentRefreshingRooms = false; + } + } _banner(SCAppGeneralManager ref) { final banners = _partyBanners(ref); @@ -771,17 +972,20 @@ class _HomePartyPageState extends State 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(); + SCChatRoomRepository() + .discovery(allRegion: true) + .then((values) { + rooms = _mergeLatestRoomsWithCurrentCounters(values); + isLoading = false; + _refreshController.refreshCompleted(); + _refreshController.loadComplete(); + if (mounted) { + setState(() {}); + unawaited(_syncVisibleRoomMemberCounts(force: true)); + } + }) + .catchError((e) { + _refreshController.loadNoData(); _refreshController.refreshCompleted(); isLoading = false; if (mounted) setState(() {}); @@ -804,17 +1008,23 @@ class _HomePartyPageState extends State }); } - _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: [ + _buildItem(SocialChatRoomRes res, int index) { + final rankBorderAsset = + index < _topRankBorderAssets.length ? _topRankBorderAssets[index] : null; + final rankBorderOverflow = 12.w; + final rankInfoHorizontalInset = rankBorderAsset != null ? 10.w : 0.w; + final rankInfoBottomInset = rankBorderAsset != null ? 9.w : 0.w; + 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( + clipBehavior: Clip.none, + alignment: Alignment.bottomCenter, + children: [ Container( padding: EdgeInsets.all(3.w), decoration: BoxDecoration( @@ -831,108 +1041,129 @@ class _HomePartyPageState extends State 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: () { + Padding( + padding: EdgeInsets.only( + left: rankInfoHorizontalInset, + right: rankInfoHorizontalInset, + bottom: rankInfoBottomInset, + ), + child: 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.displayMemberCount, + fontSize: 10.sp, + lineHeight: 1, + ) + : Container(height: 10.w), + SizedBox(width: 10.w), + ], + ), + ), + ), + if (rankBorderAsset != null) + Positioned.fill( + top: -rankBorderOverflow, + left: -rankBorderOverflow, + right: -rankBorderOverflow, + bottom: -rankBorderOverflow, + child: SCSvgaAssetWidget( + assetPath: rankBorderAsset, + active: true, + loop: true, + fit: BoxFit.fill, + allowDrawingOverflow: true, + ), + ), + ], + ), + ), + onTap: () { Provider.of( context, listen: false, diff --git a/lib/modules/report/report_page.dart b/lib/modules/report/report_page.dart index 3d5f175..4ec0594 100644 --- a/lib/modules/report/report_page.dart +++ b/lib/modules/report/report_page.dart @@ -36,333 +36,384 @@ class _ReportPageState extends State { String pic03 = ""; final TextEditingController _descriptionController = TextEditingController(); + @override + void dispose() { + _descriptionController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return SCDebounceWidget(child: Stack( - children: [ - Image.asset( - SCGlobalConfig.businessLogicStrategy.getReportPageBackgroundImage(), - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - fit: BoxFit.fill, - ), - Scaffold( - backgroundColor: Colors.transparent, - resizeToAvoidBottomInset: false, - appBar: SocialChatStandardAppBar( - actions: [], - title: SCAppLocalizations.of(context)!.report, - backgroundColor: Colors.transparent, + return SCDebounceWidget( + child: Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getReportPageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, ), - body: SafeArea( - top: false, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - SizedBox(width: 15.w), - text( - SCAppLocalizations.of( - context, - )!.pleaseSelectTheTypeContent, - fontSize: 14.sp, - textColor: SCGlobalConfig.businessLogicStrategy.getReportPageHintTextColor(), - ), - SizedBox(width: 15.w), - ], - ), - Container( - margin: EdgeInsets.only(top: 10.w, left: 15.w, right: 15.w), - padding: EdgeInsets.symmetric(horizontal: 15.w), - child: Column( + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + actions: [], + title: SCAppLocalizations.of(context)!.report, + backgroundColor: Colors.transparent, + ), + body: SafeArea( + top: false, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - _item(SCAppLocalizations.of(context)!.spam, 3), - _item(SCAppLocalizations.of(context)!.fraud, 4), - _item( - SCAppLocalizations.of(context)!.maliciousHarassment, - 0, + SizedBox(width: 15.w), + text( + SCAppLocalizations.of( + context, + )!.pleaseSelectTheTypeContent, + fontSize: 14.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getReportPageHintTextColor(), ), - _item(SCAppLocalizations.of(context)!.other, 1), - Row( - children: [ - text( - SCAppLocalizations.of(context)!.description, - fontSize: 14.sp, - textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor(), - ), - Spacer(), - ], - ), - SizedBox(height: 8.w), - Container( - padding: EdgeInsets.all(5.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: SCGlobalConfig.businessLogicStrategy.getReportPageContainerBackgroundColor(), - ), - child: TextField( - controller: _descriptionController, - onChanged: (text) {}, - maxLength: 1000, - maxLines: 5, - decoration: InputDecoration( - hintText: - SCAppLocalizations.of(context)!.reportInputTips, - hintStyle: TextStyle( - color: SCGlobalConfig.businessLogicStrategy.getReportPageSecondaryHintTextColor(), - fontSize: 14.sp, - ), - contentPadding: EdgeInsets.only(top: 0.w), - isDense: true, - filled: false, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - // enabledBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // focusedBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), - fillColor: Colors.white54, - counterStyle: TextStyle(color: Colors.white) - ), - style: TextStyle( - fontSize: sp(15), - color: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor(), - textBaseline: TextBaseline.alphabetic, - ), - ), - ), - SizedBox(height: 5.w), - Row( - children: [ - text( - SCAppLocalizations.of(context)!.screenshotTips, - fontSize: 14.sp, - textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor(), - ), - Spacer(), - ], - ), - SizedBox(height: 8.w), - Row( - children: [ - Expanded( - child: GestureDetector( - child: Stack( - children: [ - pic01.isNotEmpty - ? CustomCachedImage( - imageUrl: pic01, - height: 100.w, - ) - : Image.asset( - SCGlobalConfig.businessLogicStrategy.getReportPageIcon('addPic'), - height: 100.w, - ), - pic01.isNotEmpty - ? Positioned( - top: 5.w, - right: 5.w, - child: GestureDetector( - child: Image.asset( - SCGlobalConfig.businessLogicStrategy.getReportPageIcon('closePic'), - width: 14.w, - height: 14.w, - ), - onTap: () { - setState(() { - pic01 = ""; - }); - }, - ), - ) - : Container(), - ], - ), - onTap: () { - SCPickUtils.pickImage(context, ( - bool success, - String url, - ) { - if (success) { - setState(() { - pic01 = url; - }); - } - }); - }, - ), - ), - SizedBox(width: 5.w), - Expanded( - child: GestureDetector( - child: Stack( - children: [ - pic02.isNotEmpty - ? CustomCachedImage( - imageUrl: pic02, - height: 100.w, - ) - : Image.asset( - SCGlobalConfig.businessLogicStrategy.getReportPageIcon('addPic'), - height: 100.w, - ), - pic02.isNotEmpty - ? Positioned( - top: 5.w, - right: 5.w, - child: GestureDetector( - child: Image.asset( - SCGlobalConfig.businessLogicStrategy.getReportPageIcon('closePic'), - width: 14.w, - height: 14.w, - ), - onTap: () { - setState(() { - pic02 = ""; - }); - }, - ), - ) - : Container(), - ], - ), - onTap: () { - SCPickUtils.pickImage(context, ( - bool success, - String url, - ) { - if (success) { - setState(() { - pic02 = url; - }); - } - }); - }, - ), - ), - SizedBox(width: 5.w), - Expanded( - child: GestureDetector( - child: Stack( - children: [ - pic03.isNotEmpty - ? CustomCachedImage( - imageUrl: pic03, - height: 100.w, - ) - : Image.asset( - SCGlobalConfig.businessLogicStrategy.getReportPageIcon('addPic'), - height: 100.w, - ), - pic03.isNotEmpty - ? Positioned( - top: 5.w, - right: 5.w, - child: GestureDetector( - child: Image.asset( - SCGlobalConfig.businessLogicStrategy.getReportPageIcon('closePic'), - width: 14.w, - height: 14.w, - ), - onTap: () { - setState(() { - pic03 = ""; - }); - }, - ), - ) - : Container(), - ], - ), - onTap: () { - SCPickUtils.pickImage(context, ( - bool success, - String url, - ) { - if (success) { - setState(() { - pic03 = url; - }); - } - }); - }, - ), - ), - ], - ), - SizedBox(height: 15.w), + SizedBox(width: 15.w), ], ), - ), - SizedBox(height: 45.w), - SCDebounceWidget( - onTap: () async { - SCLoadingManager.show(); - String imageUrls = ""; - if (pic01.isNotEmpty) { - imageUrls = "$imageUrls,$pic01"; - } - if (pic02.isNotEmpty) { - imageUrls = "$imageUrls,$pic02"; - } - if (pic03.isNotEmpty) { - imageUrls = "$imageUrls,$pic03"; - } - SCChatRoomRepository() - .reported( - AccountStorage().getCurrentUser()?.userProfile?.id ?? - "", - widget.tageId, - selectedIndex, - reportedContent: _descriptionController.text, - imageUrls: imageUrls, - ) - .then((result) { - SCLoadingManager.hide(); - SCTts.show( - SCAppLocalizations.of(context)!.reportSucc, - ); - Navigator.of(context).pop(); - }) - .catchError((_) { - SCLoadingManager.hide(); - }); - - }, - child: Container( - height: 40.w, - width: double.infinity, - alignment: Alignment.center, - margin: EdgeInsets.symmetric(horizontal: 35.w), - decoration: BoxDecoration( - color: SCGlobalConfig.businessLogicStrategy.getReportPageSubmitButtonBackgroundColor(), - borderRadius: BorderRadius.circular(25.w), + Container( + margin: EdgeInsets.only( + top: 10.w, + left: 15.w, + right: 15.w, ), - child: Text( - SCAppLocalizations.of(context)!.submit, - style: TextStyle(color: SCGlobalConfig.businessLogicStrategy.getReportPageSubmitButtonTextColor(), fontSize: 16.sp), + padding: EdgeInsets.symmetric(horizontal: 15.w), + child: Column( + children: [ + _item(SCAppLocalizations.of(context)!.spam, 3), + _item(SCAppLocalizations.of(context)!.fraud, 4), + _item( + SCAppLocalizations.of(context)!.maliciousHarassment, + 0, + ), + _item(SCAppLocalizations.of(context)!.other, 1), + Row( + children: [ + text( + SCAppLocalizations.of(context)!.description, + fontSize: 14.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getReportPagePrimaryTextColor(), + ), + Spacer(), + ], + ), + SizedBox(height: 8.w), + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: + SCGlobalConfig.businessLogicStrategy + .getReportPageContainerBackgroundColor(), + ), + child: TextField( + controller: _descriptionController, + onChanged: (text) {}, + maxLength: 1000, + maxLines: 5, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of( + context, + )!.reportInputTips, + hintStyle: TextStyle( + color: + SCGlobalConfig.businessLogicStrategy + .getReportPageSecondaryHintTextColor(), + fontSize: 14.sp, + ), + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + fillColor: Colors.white54, + counterStyle: TextStyle(color: Colors.white), + ), + style: TextStyle( + fontSize: sp(15), + color: + SCGlobalConfig.businessLogicStrategy + .getReportPagePrimaryTextColor(), + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + SizedBox(height: 5.w), + Row( + children: [ + text( + SCAppLocalizations.of(context)!.screenshotTips, + fontSize: 14.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getReportPagePrimaryTextColor(), + ), + Spacer(), + ], + ), + SizedBox(height: 8.w), + Row( + children: [ + Expanded( + child: GestureDetector( + child: Stack( + children: [ + pic01.isNotEmpty + ? CustomCachedImage( + imageUrl: pic01, + height: 100.w, + ) + : Image.asset( + SCGlobalConfig.businessLogicStrategy + .getReportPageIcon('addPic'), + height: 100.w, + ), + pic01.isNotEmpty + ? Positioned( + top: 5.w, + right: 5.w, + child: GestureDetector( + child: Image.asset( + SCGlobalConfig + .businessLogicStrategy + .getReportPageIcon( + 'closePic', + ), + width: 14.w, + height: 14.w, + ), + onTap: () { + setState(() { + pic01 = ""; + }); + }, + ), + ) + : Container(), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + setState(() { + pic01 = url; + }); + } + }); + }, + ), + ), + SizedBox(width: 5.w), + Expanded( + child: GestureDetector( + child: Stack( + children: [ + pic02.isNotEmpty + ? CustomCachedImage( + imageUrl: pic02, + height: 100.w, + ) + : Image.asset( + SCGlobalConfig.businessLogicStrategy + .getReportPageIcon('addPic'), + height: 100.w, + ), + pic02.isNotEmpty + ? Positioned( + top: 5.w, + right: 5.w, + child: GestureDetector( + child: Image.asset( + SCGlobalConfig + .businessLogicStrategy + .getReportPageIcon( + 'closePic', + ), + width: 14.w, + height: 14.w, + ), + onTap: () { + setState(() { + pic02 = ""; + }); + }, + ), + ) + : Container(), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + setState(() { + pic02 = url; + }); + } + }); + }, + ), + ), + SizedBox(width: 5.w), + Expanded( + child: GestureDetector( + child: Stack( + children: [ + pic03.isNotEmpty + ? CustomCachedImage( + imageUrl: pic03, + height: 100.w, + ) + : Image.asset( + SCGlobalConfig.businessLogicStrategy + .getReportPageIcon('addPic'), + height: 100.w, + ), + pic03.isNotEmpty + ? Positioned( + top: 5.w, + right: 5.w, + child: GestureDetector( + child: Image.asset( + SCGlobalConfig + .businessLogicStrategy + .getReportPageIcon( + 'closePic', + ), + width: 14.w, + height: 14.w, + ), + onTap: () { + setState(() { + pic03 = ""; + }); + }, + ), + ) + : Container(), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + setState(() { + pic03 = url; + }); + } + }); + }, + ), + ), + ], + ), + SizedBox(height: 15.w), + ], ), ), - ), - SizedBox(height: 35.w), - ], + SizedBox(height: 45.w), + SCDebounceWidget( + onTap: () async { + SCLoadingManager.show(); + String imageUrls = ""; + if (pic01.isNotEmpty) { + imageUrls = "$imageUrls,$pic01"; + } + if (pic02.isNotEmpty) { + imageUrls = "$imageUrls,$pic02"; + } + if (pic03.isNotEmpty) { + imageUrls = "$imageUrls,$pic03"; + } + SCChatRoomRepository() + .reported( + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id ?? + "", + widget.tageId, + selectedIndex, + reportedContent: _descriptionController.text, + imageUrls: imageUrls, + ) + .then((result) { + SCLoadingManager.hide(); + SCTts.show( + SCAppLocalizations.of(context)!.reportSucc, + ); + Navigator.of(context).pop(); + }) + .catchError((_) { + SCLoadingManager.hide(); + }); + }, + child: Container( + height: 40.w, + width: double.infinity, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 35.w), + decoration: BoxDecoration( + color: + SCGlobalConfig.businessLogicStrategy + .getReportPageSubmitButtonBackgroundColor(), + borderRadius: BorderRadius.circular(25.w), + ), + child: Text( + SCAppLocalizations.of(context)!.submit, + style: TextStyle( + color: + SCGlobalConfig.businessLogicStrategy + .getReportPageSubmitButtonTextColor(), + fontSize: 16.sp, + ), + ), + ), + ), + SizedBox(height: 35.w), + ], + ), ), ), ), - ), - ], - ), onTap: (){ - FocusScope.of(context).unfocus(); - }); + ], + ), + onTap: () { + FocusScope.of(context).unfocus(); + }, + ); } Widget _item(String str, int index) { @@ -381,11 +432,19 @@ class _ReportPageState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - text(str, fontSize: 14.sp, textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor()), + text( + str, + fontSize: 14.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getReportPagePrimaryTextColor(), + ), Spacer(), selectedIndex == index ? Image.asset( - SCGlobalConfig.businessLogicStrategy.getReportPageIcon('checked'), + SCGlobalConfig.businessLogicStrategy.getReportPageIcon( + 'checked', + ), width: 20.w, fit: BoxFit.fitWidth, ) @@ -394,7 +453,12 @@ class _ReportPageState extends State { height: 20.w, decoration: BoxDecoration( shape: BoxShape.circle, - border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getReportPageUnselectedBorderColor(), width: 2.w), + border: Border.all( + color: + SCGlobalConfig.businessLogicStrategy + .getReportPageUnselectedBorderColor(), + width: 2.w, + ), ), ), ], diff --git a/lib/modules/room/edit/room_edit_page.dart b/lib/modules/room/edit/room_edit_page.dart index fa1e160..a4bfee0 100644 --- a/lib/modules/room/edit/room_edit_page.dart +++ b/lib/modules/room/edit/room_edit_page.dart @@ -78,6 +78,13 @@ class _RoomEditPageState extends State { rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""; } + @override + void dispose() { + _roomNameController.dispose(); + _roomAnnouncementController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return WillPopScope( diff --git a/lib/modules/room/rank/room_gift_rank_page.dart b/lib/modules/room/rank/room_gift_rank_page.dart index 8e65676..1dcee4d 100644 --- a/lib/modules/room/rank/room_gift_rank_page.dart +++ b/lib/modules/room/rank/room_gift_rank_page.dart @@ -39,6 +39,12 @@ class _RoomGiftRankPageState extends State _tabController = TabController(length: _pages.length, vsync: this); } + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { _tabs.clear(); @@ -97,5 +103,4 @@ class _RoomGiftRankPageState extends State ), ); } - } diff --git a/lib/modules/room/seat/sc_seat_item.dart b/lib/modules/room/seat/sc_seat_item.dart index a54583c..c10f24a 100644 --- a/lib/modules/room/seat/sc_seat_item.dart +++ b/lib/modules/room/seat/sc_seat_item.dart @@ -33,6 +33,8 @@ class _SCSeatItemState extends State with TickerProviderStateMixin { RtcProvider? provider; JoinRoomRes? room; MicRes? roomSeat; + JoinRoomRes? _cachedRoom; + MicRes? _cachedRoomSeat; final GlobalKey _targetKey = GlobalKey(); @override @@ -44,8 +46,22 @@ class _SCSeatItemState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - roomSeat = provider?.roomWheatMap[widget.index]; - room = provider?.currenRoom; + final liveRoomSeat = provider?.roomWheatMap[widget.index]; + final liveRoom = provider?.currenRoom; + if (provider?.isExitingCurrentVoiceRoomSession ?? false) { + roomSeat = liveRoomSeat ?? _cachedRoomSeat; + room = liveRoom ?? _cachedRoom; + } else { + roomSeat = liveRoomSeat; + room = liveRoom; + if (roomSeat != null) { + _cachedRoomSeat = roomSeat; + } + if (room != null) { + _cachedRoom = room; + } + } + return GestureDetector( behavior: HitTestBehavior.opaque, child: Column( @@ -78,7 +94,7 @@ class _SCSeatItemState extends State with TickerProviderStateMixin { ? "" : roomSeat?.user?.getHeaddress()?.sourceUrl, ) - : (roomSeat!.micLock! + : ((roomSeat?.micLock ?? false) ? Image.asset( "sc_images/room/sc_icon_seat_lock.png", width: widget.isGameModel ? 38.w : 52.w, @@ -93,7 +109,7 @@ class _SCSeatItemState extends State with TickerProviderStateMixin { bottom: widget.isGameModel ? 2.w : 5.w, right: widget.isGameModel ? 2.w : 5.w, child: - roomSeat!.micMute! + (roomSeat?.micMute ?? false) ? Image.asset( "sc_images/room/sc_icon_room_seat_mic_mute.png", width: 14.w, diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 57d3914..3d98a86 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -2,7 +2,6 @@ import 'dart:math' as math; 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/ui_kit/components/sc_compontent.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; @@ -53,7 +52,6 @@ class _VoiceRoomPageState extends State late TabController _tabController; final List _pages = [AllChatPage(), ChatPage(), GiftChatPage()]; - final List _tabs = []; late StreamSubscription _subscription; final RoomGiftSeatFlightController _giftSeatFlightController = RoomGiftSeatFlightController(); @@ -65,7 +63,7 @@ class _VoiceRoomPageState extends State super.initState(); _tabController = TabController(length: _pages.length, vsync: this); _enableRoomVisualEffects(); - _tabController.addListener(() {}); // 监听切换 + _tabController.addListener(_handleTabChange); _subscription = eventBus.on().listen(( event, @@ -91,11 +89,19 @@ class _VoiceRoomPageState extends State @override void dispose() { _suspendRoomVisualEffects(); + _tabController.removeListener(_handleTabChange); _tabController.dispose(); // 释放资源 _subscription.cancel(); super.dispose(); } + void _handleTabChange() { + if (!mounted) { + return; + } + setState(() {}); + } + void _enableRoomVisualEffects() { Provider.of( context, @@ -150,10 +156,6 @@ class _VoiceRoomPageState extends State @override Widget build(BuildContext context) { - _tabs.clear(); - _tabs.add(Tab(text: SCAppLocalizations.of(context)!.all)); - _tabs.add(Tab(text: SCAppLocalizations.of(context)!.chat)); - _tabs.add(Tab(text: SCAppLocalizations.of(context)!.gift)); return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) { @@ -236,20 +238,9 @@ class _VoiceRoomPageState extends State children: [ TabBar( tabAlignment: TabAlignment.start, - labelPadding: SCGlobalConfig.businessLogicStrategy - .getVoiceRoomTabLabelPadding() - .copyWith( - left: - SCGlobalConfig.businessLogicStrategy - .getVoiceRoomTabLabelPadding() - .left * - ScreenUtil().setWidth(1), - right: - SCGlobalConfig.businessLogicStrategy - .getVoiceRoomTabLabelPadding() - .right * - ScreenUtil().setWidth(1), - ), + splashFactory: NoSplash.splashFactory, + overlayColor: WidgetStateProperty.all(Colors.transparent), + labelPadding: EdgeInsets.symmetric(horizontal: 8.w), labelColor: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabLabelColor(), @@ -283,7 +274,7 @@ class _VoiceRoomPageState extends State SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabDividerColor(), controller: _tabController, - tabs: _tabs, + tabs: List.generate(_pages.length, _buildImageTab), ), Expanded( child: Container( @@ -314,6 +305,38 @@ class _VoiceRoomPageState extends State ); } + Widget _buildImageTab(int index) { + return Tab( + height: 32.w, + child: IgnorePointer( + child: Image.asset( + _roomChatTabAsset(index), + height: 26.w, + fit: BoxFit.contain, + ), + ), + ); + } + + String _roomChatTabAsset(int index) { + final bool selected = _tabController.index == index; + switch (index) { + case 0: + return selected + ? "sc_images/room/sc_icon_room_chat_tab_all_selected.png" + : "sc_images/room/sc_icon_room_chat_tab_all_unselected.png"; + case 1: + return selected + ? "sc_images/room/sc_icon_room_chat_tab_chat_selected.png" + : "sc_images/room/sc_icon_room_chat_tab_chat_unselected.png"; + case 2: + default: + return selected + ? "sc_images/room/sc_icon_room_chat_tab_gift_selected.png" + : "sc_images/room/sc_icon_room_chat_tab_gift_unselected.png"; + } + } + ///礼物上飘动画 _floatingGiftListener(Msg msg) { if (!Provider.of( diff --git a/lib/modules/search/sc_search_page.dart b/lib/modules/search/sc_search_page.dart index 0c1e82d..405f2a4 100644 --- a/lib/modules/search/sc_search_page.dart +++ b/lib/modules/search/sc_search_page.dart @@ -546,13 +546,13 @@ class _SearchRoomListState extends State { (e.extValues?.roomSetting?.password?.isEmpty ?? false) ? SizedBox(width: 3.w) : Container(height: 10.w), - (e.extValues?.roomSetting?.password?.isEmpty ?? false) - ? text( - e.extValues?.memberQuantity ?? "0", - fontSize: 10.sp, - lineHeight: 1, - ) - : Container(height: 10.w), + (e.extValues?.roomSetting?.password?.isEmpty ?? false) + ? text( + e.displayMemberCount, + fontSize: 10.sp, + lineHeight: 1, + ) + : Container(height: 10.w), SizedBox(width: 10.w), ], ), diff --git a/lib/modules/user/level/level_page.dart b/lib/modules/user/level/level_page.dart index f4fd9e3..9d5cc8b 100644 --- a/lib/modules/user/level/level_page.dart +++ b/lib/modules/user/level/level_page.dart @@ -29,12 +29,18 @@ class _LevelPageState extends State }); } + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Stack( children: [ Image.asset( - "sc_images/person/sc_icon_edit_userinfo_bg.png", + "sc_images/person/sc_icon_edit_userinfo_bg.png", width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, fit: BoxFit.fill, diff --git a/lib/modules/user/my_items/theme/bags_tab_theme_page.dart b/lib/modules/user/my_items/theme/bags_tab_theme_page.dart index d9628cd..1a3615d 100644 --- a/lib/modules/user/my_items/theme/bags_tab_theme_page.dart +++ b/lib/modules/user/my_items/theme/bags_tab_theme_page.dart @@ -7,8 +7,6 @@ import 'package:yumi/modules/user/my_items/theme/bags_theme_page.dart'; ///背包-房间主题 class BagsTabThemePage extends StatefulWidget { - - @override _BagsTabThemePageState createState() => _BagsTabThemePageState(); } @@ -22,14 +20,18 @@ class _BagsTabThemePageState extends State @override void initState() { super.initState(); - _pages.add( - BagsThemePage(), - ); + _pages.add(BagsThemePage()); _pages.add(RoomThemeCustomPage()); _tabController = TabController(length: _pages.length, vsync: this); _tabController.addListener(() {}); // 监听切换 } + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { _tabs.clear(); diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index 93cc213..3f5c631 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -160,6 +160,13 @@ class _PersonDetailPageState extends State return Tab(text: text); } + @override + void dispose() { + _tabController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + Widget _buildProfileHeader( SocialChatUserProfileManager ref, List backgroundPhotos, diff --git a/lib/modules/user/settings/account/set/pwd/reset_pwd_page.dart b/lib/modules/user/settings/account/set/pwd/reset_pwd_page.dart index 8b9583d..19e9e74 100644 --- a/lib/modules/user/settings/account/set/pwd/reset_pwd_page.dart +++ b/lib/modules/user/settings/account/set/pwd/reset_pwd_page.dart @@ -33,6 +33,14 @@ class _ResetPwdPageState extends State { ///旧密码 TextEditingController oldPassController = TextEditingController(); + @override + void dispose() { + confirmController.dispose(); + passController.dispose(); + oldPassController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Stack( @@ -50,289 +58,308 @@ class _ResetPwdPageState extends State { title: SCAppLocalizations.of(context)!.resetLoginPassword, actions: [], ), - body: SafeArea(top: false,child: Column( - children: [ - Container( - width: ScreenUtil().screenWidth, - padding: EdgeInsets.symmetric(vertical: 8.w, horizontal: 8.w), - child: text( - SCAppLocalizations.of(context)!.resetLoginPasswordtTips1, - maxLines: 2, - textColor: Colors.white, - fontSize: 10.sp, - ), - ), - SizedBox(height: 10.w), - Row( - children: [ - SizedBox(width: 15.w), - text( - SCAppLocalizations.of(context)!.account, - fontSize: 15.sp, + body: SafeArea( + top: false, + child: Column( + children: [ + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric(vertical: 8.w, horizontal: 8.w), + child: text( + SCAppLocalizations.of(context)!.resetLoginPasswordtTips1, + maxLines: 2, textColor: Colors.white, + fontSize: 10.sp, ), - ], - ), - Container( - width: ScreenUtil().screenWidth, - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), - margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Color(0xff18F2B1).withOpacity(0.1) ), - child: text( - AccountStorage().getCurrentUser()?.userProfile?.account ?? "", - textColor: Colors.white54, - fontSize: 14.sp, - ), - ), - SizedBox(height: 10.w), - Row( - children: [ - SizedBox(width: 15.w), - text( - SCAppLocalizations.of(context)!.inputYourOldPassword, - fontSize: 15.sp, - textColor: Colors.white, - ), - ], - ), - Container( - width: ScreenUtil().screenWidth, - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), - margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Color(0xff18F2B1).withOpacity(0.1) - ), - child: TextField( - controller: oldPassController, - maxLength: 30, - maxLines: 1, - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp(r'\s')) - ], - - obscureText: !showPass3, - textInputAction: TextInputAction.done, - onChanged: (s) { - setState(() {}); - }, - decoration: InputDecoration( - hintText: - SCAppLocalizations.of(context)!.enterYourOldPassword, - hintStyle: TextStyle( - color: Colors.white54, - fontSize: 14.sp, - ), - counterText: '', - contentPadding: EdgeInsets.only(top: 0.w), - isDense: true, - filled: false, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - suffix: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - setState(() { - showPass3 = !showPass3; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Image.asset( - !showPass3 - ? "sc_images/login/sc_icon_pass.png" - : "sc_images/login/sc_icon_pass1.png", - gaplessPlayback: true, - color: Colors.white54, - width: 15.w, - ), - ), - ), - ), - style: TextStyle(fontSize: 12.sp, color: Colors.white), - ), - ), - SizedBox(height: 10.w), - Row( - children: [ - SizedBox(width: 15.w), - text( - SCAppLocalizations.of(context)!.setYourPassword, - fontSize: 15.sp, - textColor: Colors.white, - ), - ], - ), - Container( - width: ScreenUtil().screenWidth, - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), - margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Color(0xff18F2B1).withOpacity(0.1) - ), - child: TextField( - controller: passController, - maxLength: 30, - maxLines: 1, - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp(r'\s')) - ], - - obscureText: !showPass, - textInputAction: TextInputAction.done, - onChanged: (s) { - setState(() {}); - }, - decoration: InputDecoration( - hintText: - SCAppLocalizations.of(context)!.enterYourNewPassword, - hintStyle: TextStyle( - color: Colors.white54, - fontSize: 14.sp, - ), - counterText: '', - contentPadding: EdgeInsets.only(top: 0.w), - isDense: true, - filled: false, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - suffix: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - setState(() { - showPass = !showPass; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Image.asset( - !showPass - ? "sc_images/login/sc_icon_pass.png" - : "sc_images/login/sc_icon_pass1.png", - gaplessPlayback: true, - color: Colors.white54, - width: 15.w, - ), - ), - ), - ), - style: TextStyle(fontSize: 12.sp, color: Colors.white), - ), - ), - Container( - width: ScreenUtil().screenWidth, - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), - margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Colors.black12, - ), - child: TextField( - controller: confirmController, - maxLength: 30, - maxLines: 1, - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp(r'\s')) - ], - - obscureText: !showPass2, - textInputAction: TextInputAction.done, - onChanged: (s) { - setState(() {}); - }, - decoration: InputDecoration( - hintText: SCAppLocalizations.of(context)!.confirmYourPassword, - hintStyle: TextStyle( - color: Colors.white54, - fontSize: 14.sp, - ), - counterText: '', - contentPadding: EdgeInsets.only(top: 0.w), - isDense: true, - filled: false, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - suffix: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - setState(() { - showPass2 = !showPass2; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Image.asset( - !showPass2 - ? "sc_images/login/sc_icon_pass.png" - : "sc_images/login/sc_icon_pass1.png", - gaplessPlayback: true, - color: Colors.black54, - width: 15.w, - ), - ), - ), - ), - style: TextStyle(fontSize: 12.sp, color: Colors.white), - ), - ), - Row( - children: [ - SizedBox(width: 10.w), - Expanded( - child: text( - SCAppLocalizations.of(context)!.resetLoginPasswordtTips2, - fontSize: 10.sp, + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.account, + fontSize: 15.sp, textColor: Colors.white, - maxLines: 3, ), + ], + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.w, ), - SizedBox(width: 10.w), - ], - ), - SizedBox(height: 20.w), - GestureDetector( - child: Container( - height: 44.w, - width: 260.w, - alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - color: SocialChatTheme.primaryLight, + color: Color(0xff18F2B1).withOpacity(0.1), ), child: text( - SCAppLocalizations.of(context)!.submit, - fontSize: 16.sp, - textColor: Colors.white, + AccountStorage().getCurrentUser()?.userProfile?.account ?? + "", + textColor: Colors.white54, + fontSize: 14.sp, ), ), - onTap: () { - resetPwd(); - }, - ), - ], - ),), + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.inputYourOldPassword, + fontSize: 15.sp, + textColor: Colors.white, + ), + ], + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.w, + ), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: TextField( + controller: oldPassController, + maxLength: 30, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')), + ], + + obscureText: !showPass3, + textInputAction: TextInputAction.done, + onChanged: (s) { + setState(() {}); + }, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.enterYourOldPassword, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + counterText: '', + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + suffix: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + showPass3 = !showPass3; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset( + !showPass3 + ? "sc_images/login/sc_icon_pass.png" + : "sc_images/login/sc_icon_pass1.png", + gaplessPlayback: true, + color: Colors.white54, + width: 15.w, + ), + ), + ), + ), + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.setYourPassword, + fontSize: 15.sp, + textColor: Colors.white, + ), + ], + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.w, + ), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: TextField( + controller: passController, + maxLength: 30, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')), + ], + + obscureText: !showPass, + textInputAction: TextInputAction.done, + onChanged: (s) { + setState(() {}); + }, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.enterYourNewPassword, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + counterText: '', + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + suffix: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + showPass = !showPass; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset( + !showPass + ? "sc_images/login/sc_icon_pass.png" + : "sc_images/login/sc_icon_pass1.png", + gaplessPlayback: true, + color: Colors.white54, + width: 15.w, + ), + ), + ), + ), + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.w, + ), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.black12, + ), + child: TextField( + controller: confirmController, + maxLength: 30, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')), + ], + + obscureText: !showPass2, + textInputAction: TextInputAction.done, + onChanged: (s) { + setState(() {}); + }, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.confirmYourPassword, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + counterText: '', + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + suffix: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + showPass2 = !showPass2; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset( + !showPass2 + ? "sc_images/login/sc_icon_pass.png" + : "sc_images/login/sc_icon_pass1.png", + gaplessPlayback: true, + color: Colors.black54, + width: 15.w, + ), + ), + ), + ), + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + Row( + children: [ + SizedBox(width: 10.w), + Expanded( + child: text( + SCAppLocalizations.of( + context, + )!.resetLoginPasswordtTips2, + fontSize: 10.sp, + textColor: Colors.white, + maxLines: 3, + ), + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 20.w), + GestureDetector( + child: Container( + height: 44.w, + width: 260.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.submit, + fontSize: 16.sp, + textColor: Colors.white, + ), + ), + onTap: () { + resetPwd(); + }, + ), + ], + ), + ), ), ], ); diff --git a/lib/modules/user/settings/account/set/pwd/set_pwd_page.dart b/lib/modules/user/settings/account/set/pwd/set_pwd_page.dart index 1245eee..0c47720 100644 --- a/lib/modules/user/settings/account/set/pwd/set_pwd_page.dart +++ b/lib/modules/user/settings/account/set/pwd/set_pwd_page.dart @@ -29,6 +29,13 @@ class _SetPwdPageState extends State { ///密码控制器 TextEditingController passController = TextEditingController(); + @override + void dispose() { + confirmController.dispose(); + passController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Stack( diff --git a/lib/modules/user/settings/blacklist/user_blacklist_page.dart b/lib/modules/user/settings/blacklist/user_blacklist_page.dart index d419b74..d65ff29 100644 --- a/lib/modules/user/settings/blacklist/user_blacklist_page.dart +++ b/lib/modules/user/settings/blacklist/user_blacklist_page.dart @@ -45,6 +45,12 @@ class _UserBlockedListPageState loadData(1); } + @override + void dispose() { + _textEditingController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Stack( @@ -118,13 +124,15 @@ class _UserBlockedListPageState child: Row( children: [ SizedBox(width: 10.w), - SCDebounceWidget(child: head(url: res.userProfile?.userAvatar ?? "", width: 62.w), onTap: (){ - SCNavigatorUtils.push( - context, - "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.userProfile?.id}&tageId=${res.userProfile?.id}", - ); - }) - , + SCDebounceWidget( + child: head(url: res.userProfile?.userAvatar ?? "", width: 62.w), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.userProfile?.id}&tageId=${res.userProfile?.id}", + ); + }, + ), SizedBox(width: 10.w), Expanded( child: Column( @@ -134,11 +142,14 @@ class _UserBlockedListPageState children: [ netImage( url: - Provider.of(context, listen: false) - .findCountryByName( - res.userProfile?.countryName ?? "", - ) - ?.nationalFlag ?? + Provider.of( + context, + listen: false, + ) + .findCountryByName( + res.userProfile?.countryName ?? "", + ) + ?.nationalFlag ?? "", borderRadius: BorderRadius.all(Radius.circular(3.w)), width: 19.w, @@ -150,14 +161,15 @@ class _UserBlockedListPageState textColor: Colors.white, res.userProfile?.userNickname ?? "", fontSize: 14.sp, - type:res.userProfile?.getVIP()?.name ?? "", + type: res.userProfile?.getVIP()?.name ?? "", needScroll: - (res.userProfile?.userNickname?.characters.length ?? 0) > + (res.userProfile?.userNickname?.characters.length ?? + 0) > 10, ), SizedBox(width: 3.w), Container( - width: (res.userProfile?.age??0)>999?58.w:48.w, + width: (res.userProfile?.age ?? 0) > 999 ? 58.w : 48.w, height: 24.w, decoration: BoxDecoration( image: DecorationImage( @@ -234,13 +246,15 @@ class _UserBlockedListPageState ], ), ), - SCDebounceWidget(child: Image.asset( - "sc_images/room/sc_icon_block_list_delete.png", - height: 20.w, - ), onTap: (){ - _delete(res.userProfile); - }) - , + SCDebounceWidget( + child: Image.asset( + "sc_images/room/sc_icon_block_list_delete.png", + height: 20.w, + ), + onTap: () { + _delete(res.userProfile); + }, + ), SizedBox(width: 15.w), ], ), @@ -257,7 +271,7 @@ class _UserBlockedListPageState setState(() {}); }) .catchError((e) { - SCLoadingManager.hide(); + SCLoadingManager.hide(); loadComplete(); setState(() {}); }); @@ -307,46 +321,29 @@ class _UserBlockedListPageState void _delete(SocialChatUserProfile? userProfile) { SmartDialog.show( tag: "showConfirmDialog", - alignment: - Alignment.center, + alignment: Alignment.center, debounce: true, - animationType: - SmartAnimationType - .fade, + animationType: SmartAnimationType.fade, builder: (_) { return MsgDialog( - title: - SCAppLocalizations.of( - context!, - )!.tips, - msg:SCAppLocalizations.of( - context, - )!.areYouSureToCancelBlacklist, - btnText: - SCAppLocalizations.of( - context, - )!.confirm, + title: SCAppLocalizations.of(context!)!.tips, + msg: SCAppLocalizations.of(context)!.areYouSureToCancelBlacklist, + btnText: SCAppLocalizations.of(context)!.confirm, onEnsure: () { SCLoadingManager.show(); SCAccountRepository() - .deleteUserBlacklist( - userProfile?.id??"", - ) - .then(( - result, - ) { - SCTts.show( - SCAppLocalizations.of( - context, - )!.successfullyRemovedFromTheBlacklist, - ); - loadData(1); - }) - .catchError(( - e, - ) { - SCLoadingManager.hide(); - }); + .deleteUserBlacklist(userProfile?.id ?? "") + .then((result) { + SCTts.show( + SCAppLocalizations.of( + context, + )!.successfullyRemovedFromTheBlacklist, + ); + loadData(1); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); }, ); }, diff --git a/lib/modules/wallet/recharge/mifa_pay_webview_page.dart b/lib/modules/wallet/recharge/mifa_pay_webview_page.dart new file mode 100644 index 0000000..ffff7f1 --- /dev/null +++ b/lib/modules/wallet/recharge/mifa_pay_webview_page.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:yumi/shared/tools/sc_url_launcher_utils.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class MiFaPayWebViewPage extends StatefulWidget { + const MiFaPayWebViewPage({ + super.key, + required this.requestUrl, + this.title = 'MiFaPay', + }); + + final String requestUrl; + final String title; + + @override + State createState() => _MiFaPayWebViewPageState(); +} + +class _MiFaPayWebViewPageState extends State { + late final WebViewController _controller; + + bool _isLoading = true; + double _progress = 0; + + @override + void initState() { + super.initState(); + _controller = + WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (NavigationRequest request) async { + if (!SCUrlLauncherUtils.shouldOpenExternally(request.url)) { + return NavigationDecision.navigate; + } + + final bool launched = await SCUrlLauncherUtils.launchExternal( + request.url, + ); + if (launched) { + return NavigationDecision.prevent; + } + + return SCUrlLauncherUtils.isHttpUrl(request.url) + ? NavigationDecision.navigate + : NavigationDecision.prevent; + }, + onProgress: (int progress) { + _updateState(() { + _progress = progress / 100; + }); + }, + onPageStarted: (_) { + _updateState(() { + _isLoading = true; + }); + }, + onPageFinished: (_) { + _updateState(() { + _isLoading = false; + _progress = 1; + }); + }, + onWebResourceError: (_) { + _updateState(() { + _isLoading = false; + }); + }, + ), + ) + ..loadRequest(Uri.parse(widget.requestUrl)); + } + + @override + void dispose() { + super.dispose(); + } + + void _updateState(VoidCallback action) { + if (!mounted) { + return; + } + setState(action); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: SocialChatStandardAppBar( + title: widget.title, + actions: const [], + backgroundColor: Colors.white, + backButtonColor: Colors.black, + gradient: const LinearGradient( + colors: [Colors.white, Colors.white], + ), + ), + body: Stack( + children: [ + WebViewWidget(controller: _controller), + if (_isLoading || _progress < 1) + Positioned( + top: 0, + left: 0, + right: 0, + child: LinearProgressIndicator( + value: _progress > 0 && _progress < 1 ? _progress : null, + minHeight: 2, + color: const Color(0xff18F2B1), + backgroundColor: const Color(0xffEDEDED), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/wallet/recharge/recharge_method_bottom_sheet.dart b/lib/modules/wallet/recharge/recharge_method_bottom_sheet.dart new file mode 100644 index 0000000..3cb67a1 --- /dev/null +++ b/lib/modules/wallet/recharge/recharge_method_bottom_sheet.dart @@ -0,0 +1,463 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +enum RechargeMethodType { miFaPay, nativeStore } + +class RechargeMethodOption { + const RechargeMethodOption({ + required this.type, + required this.title, + this.assetIconPath, + this.iconData, + }); + + final RechargeMethodType type; + final String title; + final String? assetIconPath; + final IconData? iconData; +} + +class RechargePackageOption { + const RechargePackageOption({ + required this.id, + required this.coins, + required this.bonusCoins, + required this.priceLabel, + required this.badge, + }); + + final String id; + final int coins; + final int bonusCoins; + final String priceLabel; + final String badge; +} + +class RechargeChannelOption { + const RechargeChannelOption({ + required this.code, + required this.name, + this.typeLabel = '', + }); + + final String code; + final String name; + final String typeLabel; +} + +class RechargeMethodBottomSheet extends StatefulWidget { + const RechargeMethodBottomSheet({ + super.key, + required this.method, + required this.packages, + required this.channels, + required this.onConfirm, + this.initialPackageId, + this.initialChannelCode, + }); + + final RechargeMethodOption method; + final List packages; + final List channels; + final void Function( + RechargePackageOption package, + RechargeChannelOption channel, + ) + onConfirm; + final String? initialPackageId; + final String? initialChannelCode; + + @override + State createState() => + _RechargeMethodBottomSheetState(); +} + +class _RechargeMethodBottomSheetState extends State { + late String _selectedPackageId; + late String _selectedChannelCode; + + @override + void initState() { + super.initState(); + _selectedPackageId = + widget.packages.isEmpty + ? '' + : widget.initialPackageId ?? widget.packages.first.id; + _selectedChannelCode = + widget.channels.isEmpty + ? '' + : widget.initialChannelCode ?? widget.channels.first.code; + } + + @override + Widget build(BuildContext context) { + final bool canConfirm = + widget.packages.isNotEmpty && + widget.channels.isNotEmpty && + _selectedPackageId.isNotEmpty && + _selectedChannelCode.isNotEmpty; + + return SafeArea( + top: false, + child: Container( + width: ScreenUtil().screenWidth, + height: MediaQuery.of(context).size.height * 0.72, + padding: EdgeInsets.fromLTRB(14.w, 14.w, 14.w, 14.w), + decoration: BoxDecoration( + color: const Color(0xff03523a), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(14.w), + topRight: Radius.circular(14.w), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + widget.method.title, + style: TextStyle( + fontSize: 18.sp, + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + ), + SCDebounceWidget( + onTap: () => Navigator.of(context).pop(), + child: Icon( + CupertinoIcons.clear_thick_circled, + size: 22.w, + color: Colors.white.withValues(alpha: 0.86), + ), + ), + ], + ), + SizedBox(height: 6.w), + Text( + 'Select amount', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white.withValues(alpha: 0.68), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 16.w), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.packages.isEmpty) + _buildEmptyHint('MiFaPay products are loading') + else + GridView.builder( + itemCount: widget.packages.length, + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 10.w, + crossAxisSpacing: 10.w, + mainAxisExtent: 136.w, + ), + itemBuilder: (BuildContext context, int index) { + final RechargePackageOption item = + widget.packages[index]; + final bool isSelected = item.id == _selectedPackageId; + return _buildPackageCard(item, index, isSelected); + }, + ), + SizedBox(height: 16.w), + Text( + 'Payment channel', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white.withValues(alpha: 0.68), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 10.w), + if (widget.channels.isEmpty) + _buildEmptyHint('No channel available') + else + Wrap( + spacing: 10.w, + runSpacing: 10.w, + children: widget.channels + .map(_buildChannelChip) + .toList(growable: false), + ), + ], + ), + ), + ), + SizedBox(height: 8.w), + SCDebounceWidget( + onTap: () { + if (!canConfirm) { + return; + } + _confirmSelection(); + }, + child: Container( + width: double.infinity, + height: 42.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: + canConfirm + ? SCGlobalConfig.businessLogicStrategy + .getRechargePageButtonBackgroundColor() + : Colors.white.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(10.w), + ), + child: Text( + 'Confirm', + style: TextStyle( + fontSize: 15.sp, + color: + SCGlobalConfig.businessLogicStrategy + .getRechargePageButtonTextColor(), + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _confirmSelection() { + if (widget.packages.isEmpty || widget.channels.isEmpty) { + return; + } + final RechargePackageOption selected = widget.packages.firstWhere( + (RechargePackageOption item) => item.id == _selectedPackageId, + orElse: () => widget.packages.first, + ); + final RechargeChannelOption selectedChannel = widget.channels.firstWhere( + (RechargeChannelOption item) => item.code == _selectedChannelCode, + orElse: () => widget.channels.first, + ); + widget.onConfirm(selected, selectedChannel); + Navigator.of(context).pop(); + } + + Widget _buildPackageCard( + RechargePackageOption item, + int index, + bool isSelected, + ) { + return SCDebounceWidget( + onTap: () { + setState(() { + _selectedPackageId = item.id; + }); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10.w), + border: Border.all( + color: + isSelected ? SocialChatTheme.primaryLight : Colors.transparent, + width: 1.4, + ), + ), + child: Column( + children: [ + SizedBox( + height: 24.w, + child: + item.badge.isEmpty + ? null + : Align( + alignment: Alignment.topRight, + child: Container( + margin: EdgeInsets.only(top: 8.w, right: 8.w), + padding: EdgeInsets.symmetric( + horizontal: 7.w, + vertical: 2.w, + ), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight.withValues( + alpha: 0.14, + ), + borderRadius: BorderRadius.circular(999.w), + ), + child: Text( + item.badge, + style: TextStyle( + fontSize: 9.sp, + color: const Color(0xff03523a), + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getRechargePageGoldIconByIndex(index), + width: 38.w, + height: 38.w, + ), + SizedBox(height: 6.w), + Padding( + padding: EdgeInsets.symmetric(horizontal: 6.w), + child: Text( + _formatWholeNumber(item.coins), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15.sp, + color: Colors.black.withValues(alpha: 0.82), + fontWeight: FontWeight.w700, + ), + ), + ), + SizedBox(height: 2.w), + Padding( + padding: EdgeInsets.symmetric(horizontal: 6.w), + child: Text( + item.bonusCoins > 0 + ? '+${_formatWholeNumber(item.bonusCoins)}' + : 'No bonus', + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 11.sp, + color: + item.bonusCoins > 0 + ? SocialChatTheme.primaryLight + : Colors.black.withValues(alpha: 0.45), + fontWeight: FontWeight.w600, + ), + ), + ), + const Spacer(), + Container( + width: double.infinity, + height: 26.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0xffF5C550), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(10.w), + bottomRight: Radius.circular(10.w), + ), + ), + child: Text( + item.priceLabel, + style: TextStyle( + fontSize: 11.sp, + color: const Color(0xff6F4B00), + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildChannelChip(RechargeChannelOption option) { + final bool isSelected = option.code == _selectedChannelCode; + return SCDebounceWidget( + onTap: () { + setState(() { + _selectedChannelCode = option.code; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + decoration: BoxDecoration( + color: + isSelected + ? Colors.white.withValues(alpha: 0.16) + : Colors.white.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(8.w), + border: Border.all( + color: + isSelected + ? SocialChatTheme.primaryLight + : Colors.white.withValues(alpha: 0.14), + width: 1.1, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + option.name, + style: TextStyle( + fontSize: 13.sp, + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + if (option.typeLabel.isNotEmpty) ...[ + SizedBox(height: 2.w), + Text( + option.typeLabel, + style: TextStyle( + fontSize: 10.sp, + color: Colors.white.withValues(alpha: 0.62), + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ), + ); + } + + Widget _buildEmptyHint(String message) { + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(10.w), + ), + child: Text( + message, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.sp, + color: Colors.white.withValues(alpha: 0.68), + fontWeight: FontWeight.w500, + ), + ), + ); + } + + String _formatWholeNumber(int value) { + final String raw = value.toString(); + final StringBuffer buffer = StringBuffer(); + for (int i = 0; i < raw.length; i++) { + final int position = raw.length - i; + buffer.write(raw[i]); + if (position > 1 && position % 3 == 1) { + buffer.write(','); + } + } + return buffer.toString(); + } +} diff --git a/lib/modules/wallet/recharge/recharge_page.dart b/lib/modules/wallet/recharge/recharge_page.dart index 5dc4581..f074764 100644 --- a/lib/modules/wallet/recharge/recharge_page.dart +++ b/lib/modules/wallet/recharge/recharge_page.dart @@ -1,436 +1,923 @@ -import 'dart:io'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:in_app_purchase/in_app_purchase.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:provider/provider.dart'; - -import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; -import 'package:yumi/app/constants/sc_screen.dart'; -import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/services/payment/apple_payment_manager.dart'; -import 'package:yumi/services/payment/google_payment_manager.dart'; -import 'package:yumi/services/auth/user_profile_manager.dart'; -import 'package:yumi/modules/wallet/wallet_route.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; - -class RechargePage extends StatefulWidget { - @override - _RechargePageState createState() => _RechargePageState(); -} - -class _RechargePageState extends State { - @override - void initState() { - super.initState(); - Provider.of(context, listen: false).balance(); - if (Platform.isAndroid) { - Provider.of(context, listen: false).initializePaymentProcessor(context); - } else if (Platform.isIOS) { - Provider.of(context, listen: false).initializePaymentProcessor(context); - } - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Image.asset( - SCGlobalConfig.businessLogicStrategy.getRechargePageBackgroundImage(), - width: ScreenUtil().screenWidth, - fit: BoxFit.fill, - ), - Scaffold( - backgroundColor: SCGlobalConfig.businessLogicStrategy.getRechargePageScaffoldBackgroundColor(), - resizeToAvoidBottomInset: false, - appBar: SocialChatStandardAppBar( - title: SCAppLocalizations.of(context)!.recharge, - actions: [ - GestureDetector( - child: Container( - margin: EdgeInsetsDirectional.only(end: 15.w), - child: Image.asset( - SCGlobalConfig.businessLogicStrategy.getRechargePageRecordIcon(), - width: 24.w, - height: 24.w, - ), - ), - onTap: () { - showGeneralDialog( - context: context, - barrierLabel: '', - barrierDismissible: true, - transitionDuration: Duration(milliseconds: 350), - barrierColor: SCGlobalConfig.businessLogicStrategy.getRechargePageDialogBarrierColor(), - pageBuilder: ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - ) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Navigator.pop(context); - setState(() {}); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - SizedBox(height: height(68)), - Row( - children: [ - Spacer(), - GestureDetector( - child: Container( - decoration: BoxDecoration( - color: SCGlobalConfig.businessLogicStrategy.getRechargePageDialogContainerBackgroundColor(), - borderRadius: BorderRadius.circular(4), - ), - padding: EdgeInsets.symmetric( - horizontal: 8.w, - vertical: 3.w, - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox(height: 5.w), - GestureDetector( - child: text( - SCAppLocalizations.of( - context, - )!.goldListort, - textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageDialogTextColor(), - fontSize: 14.sp, - ), - onTap: () { - Navigator.of(context).pop(); - SCNavigatorUtils.push( - context, - WalletRoute.goldRecord, - replace: false, - ); - }, - ), - // SizedBox(height: 5.w), - // Container( - // color: Colors.black26, - // width: 110.w, - // height: 0.5.w, - // ), - // SizedBox(height: 5.w,), - // text( - // SCAppLocalizations.of( - // context, - // )!.rechargeList, - // textColor: Colors.black38, - // fontSize: 14.sp, - // ), - SizedBox(height: 5.w), - ], - ), - ), - onTap: () {}, - ), - SizedBox(width: 10.w), - ], - ), - Expanded(child: SizedBox(width: width(1))), - ], - ), - ); - }, - ); - }, - ), - ], - ), - body: SafeArea( - top: false, - child: Column( - children: [ - _buildWalletSection(), - SizedBox(height: 60.w), - Expanded( - child: Container( - padding: EdgeInsets.symmetric( - vertical: 20.w, - horizontal: 12.w, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(12.0), - topRight: Radius.circular(12.0), - ), - color: SCGlobalConfig.businessLogicStrategy.getRechargePageMainContainerBackgroundColor(), - ), - child: Column( - children: [ - Expanded( - child: - Platform.isAndroid - ? Consumer( - builder: (context, ref, child) { - return ref.products.isNotEmpty - ? ListView.separated( - itemCount: - ref.products.length, // 列表项数量 - padding: EdgeInsets.only( - bottom: 15.w, - ), - itemBuilder: (context, index) { - return _buildGoogleProductItem( - ref.products[index], - ref, - index, - ); - }, - separatorBuilder: ( - BuildContext context, - int index, - ) { - return SizedBox(height: 10.w); - }, - ) - : mainEmpty(); - }, - ) - : Consumer( - builder: (context, ref, child) { - return ref.products.isNotEmpty - ? ListView.separated( - itemCount: - ref.products.length, // 列表项数量 - padding: EdgeInsets.only( - bottom: 15.w, - ), - itemBuilder: (context, index) { - return _buildAppleProductItem( - ref.products[index], - ref, - index, - ); - }, - separatorBuilder: ( - BuildContext context, - int index, - ) { - return SizedBox(height: 10.w); - }, - ) - : mainEmpty(); - }, - ), - ), - ///针对ios的恢复购买 - Platform.isIOS?SCDebounceWidget( - child: Container( - alignment: Alignment.center, - margin: EdgeInsets.symmetric(horizontal: 60.w), - height: 42.w, - decoration: BoxDecoration( - color: SCGlobalConfig.businessLogicStrategy.getRechargePageButtonBackgroundColor(), - borderRadius: BorderRadius.all( - Radius.circular(height(8)), - ), - ), - child: text( - SCAppLocalizations.of(context)!.restorePurchases, - textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageButtonTextColor(), - fontSize: 16.sp, - ), - ), - onTap: () { - Provider.of( - context, - listen: false, - ).recoverTransactions(); - }, - ):Container(), - SizedBox(height: 15.w), - SCDebounceWidget( - child: Container( - alignment: Alignment.center, - margin: EdgeInsets.symmetric(horizontal: 60.w), - height: 42.w, - decoration: BoxDecoration( - color: SCGlobalConfig.businessLogicStrategy.getRechargePageButtonBackgroundColor(), - borderRadius: BorderRadius.all( - Radius.circular(height(8)), - ), - ), - child: text( - SCAppLocalizations.of(context)!.recharge, - textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageButtonTextColor(), - fontSize: 16.sp, - ), - ), - onTap: () { - if (Platform.isAndroid) { - Provider.of( - context, - listen: false, - ).processPurchase(); - } else if (Platform.isIOS) { - Provider.of( - context, - listen: false, - ).processPurchase(); - } - }, - ), - SizedBox(height: 45.w), - ], - ), - ), - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildWalletSection() { - return Container( - margin: EdgeInsets.symmetric(horizontal: 15.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(height: 45.w), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - SCGlobalConfig.businessLogicStrategy.getRechargePageGoldIcon(), - width: 36.w, - height: 36.w, - ), - SizedBox(width: 3.w), - Consumer( - builder: (context, ref, child) { - return text( - ref.myBalance > 1000 - ? "${(ref.myBalance / 1000).toStringAsFixed(2)}k" - : "${ref.myBalance}", - fontSize: 16.sp, - textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageWalletTextColor(), - fontWeight: FontWeight.bold, - ); - }, - ), - ], - ), - ], - ), - ); - } - - - Widget _buildGoogleProductItem( - SelecteProductConfig productConfig, - AndroidPaymentProcessor ref, - int index, - ) { - return GestureDetector( - child: Container( - padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(3), - color: productConfig.isSelecte ? SCGlobalConfig.businessLogicStrategy.getRechargePageSelectedItemBackgroundColor() : SCGlobalConfig.businessLogicStrategy.getRechargePageUnselectedItemBackgroundColor(), - border: Border.all( - color: productConfig.isSelecte ? SCGlobalConfig.businessLogicStrategy.getRechargePageSelectedItemBorderColor() : SCGlobalConfig.businessLogicStrategy.getRechargePageUnselectedItemBorderColor(), - width: 1.w, - ), - ), - child: Row( - children: [ - Image.asset( - SCGlobalConfig.businessLogicStrategy.getRechargePageGoldIconByIndex(index), - width: 34.w, - height: 34.w, - ), - SizedBox(width: 5.w), - text( - "${ref.productMap[productConfig.produc.id]?.obtainCandy ?? 0}", - fontSize: 12.sp, - textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageItemTextColor(), - ), - Spacer(), - text( - productConfig.produc.price, - textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageItemPriceTextColor(), - fontWeight: FontWeight.w600, - fontSize: 12.sp, - ), - ], - ), - ), - onTap: () { - ref.chooseProductConfig(index); - }, - ); - } - - Widget _buildAppleProductItem( - SelecteProductConfig productConfig, - IOSPaymentProcessor ref, - int index, - ) { - return GestureDetector( - child: Container( - padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: SCGlobalConfig.businessLogicStrategy.getRechargePageAppleItemBackgroundColor(), - border: Border.all( - color: productConfig.isSelecte ? SCGlobalConfig.businessLogicStrategy.getRechargePageSelectedItemBorderColor() : SCGlobalConfig.businessLogicStrategy.getRechargePageUnselectedItemBorderColor(), - width: 1.w, - ), - ), - child: Row( - children: [ - Image.asset( - SCGlobalConfig.businessLogicStrategy.getRechargePageGoldIconByIndex(index), - width: 34.w, - height: 34.w, - ), - SizedBox(width: 5.w), - text( - "${ref.productMap[productConfig.produc.id]?.obtainCandy ?? 0}", - fontSize: 12.sp, - textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageItemTextColor(), - ), - Spacer(), - text( - productConfig.produc.price, - textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageItemPriceTextColor(), - fontWeight: FontWeight.w600, - fontSize: 12.sp, - ), - ], - ), - ), - onTap: () { - ref.chooseProductConfig(index); - }, - ); - } -} - -class SelecteProductConfig { - ProductDetails produc; - bool isSelecte = false; - - SelecteProductConfig(this.produc, this.isSelecte); -} +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/modules/wallet/recharge/recharge_method_bottom_sheet.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/services/payment/apple_payment_manager.dart'; +import 'package:yumi/services/payment/google_payment_manager.dart'; +import 'package:yumi/services/payment/mifa_pay_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; + +class RechargePage extends StatefulWidget { + const RechargePage({super.key}); + + @override + State createState() => _RechargePageState(); +} + +class _RechargePageState extends State { + static const Color _accentGoldColor = Color(0xffF5C550); + static const Color _surfaceTextColor = Colors.white; + static const Color _surfaceSubtleTextColor = Color(0xCCFFFFFF); + + late final List _methodOptions; + + RechargeMethodType _selectedMethodType = RechargeMethodType.nativeStore; + + @override + void initState() { + super.initState(); + _methodOptions = _buildMethodOptions(); + Provider.of(context, listen: false).balance(); + Provider.of(context, listen: false).initialize(); + if (Platform.isAndroid) { + Provider.of( + context, + listen: false, + ).initializePaymentProcessor(context); + } else if (Platform.isIOS) { + Provider.of( + context, + listen: false, + ).initializePaymentProcessor(context); + } + } + + @override + Widget build(BuildContext context) { + final MiFaPayManager miFaPayManager = context.watch(); + + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getRechargePageBackgroundImage(), + width: ScreenUtil().screenWidth, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: + SCGlobalConfig.businessLogicStrategy + .getRechargePageScaffoldBackgroundColor(), + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.recharge, + actions: [ + GestureDetector( + onTap: _showRecordDialog, + child: Container( + margin: EdgeInsetsDirectional.only(end: 15.w), + child: Image.asset( + SCGlobalConfig.businessLogicStrategy + .getRechargePageRecordIcon(), + width: 24.w, + height: 24.w, + ), + ), + ), + ], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + _buildWalletSection(), + SizedBox(height: 36.w), + Expanded( + child: Padding( + padding: EdgeInsets.fromLTRB(12.w, 8.w, 12.w, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMethodSection(miFaPayManager), + SizedBox(height: 14.w), + Expanded( + child: + _selectedMethodType == + RechargeMethodType.nativeStore + ? _buildNativeProductList() + : _buildMiFaPayContent(miFaPayManager), + ), + if (_selectedMethodType == + RechargeMethodType.nativeStore && + Platform.isIOS) ...[ + SizedBox(height: 12.w), + _buildFooterButton( + title: + SCAppLocalizations.of( + context, + )!.restorePurchases, + onTap: () { + Provider.of( + context, + listen: false, + ).recoverTransactions(); + }, + ), + ], + SizedBox(height: 12.w), + _buildFooterButton( + title: + _selectedMethodType == + RechargeMethodType.nativeStore + ? SCAppLocalizations.of(context)!.recharge + : _buildMiFaPayPrimaryTitle(miFaPayManager), + onTap: () { + if (_selectedMethodType == + RechargeMethodType.nativeStore) { + _processNativePurchase(); + return; + } + _handleMiFaPayPrimaryAction(miFaPayManager); + }, + ), + SizedBox(height: 14.w), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ); + } + + List _buildMethodOptions() { + return [ + RechargeMethodOption( + type: RechargeMethodType.nativeStore, + title: Platform.isAndroid ? 'Google Pay' : 'Apple Pay', + assetIconPath: + Platform.isAndroid ? 'sc_images/login/sc_icon_google.png' : null, + iconData: Platform.isIOS ? Icons.apple : null, + ), + const RechargeMethodOption( + type: RechargeMethodType.miFaPay, + title: 'MiFaPay', + assetIconPath: 'sc_images/index/sc_icon_recharge_agency.png', + ), + ]; + } + + void _showRecordDialog() { + showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: true, + transitionDuration: const Duration(milliseconds: 350), + barrierColor: + SCGlobalConfig.businessLogicStrategy + .getRechargePageDialogBarrierColor(), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.pop(context); + setState(() {}); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox(height: height(68)), + Row( + children: [ + const Spacer(), + GestureDetector( + child: Container( + decoration: BoxDecoration( + color: + SCGlobalConfig.businessLogicStrategy + .getRechargePageDialogContainerBackgroundColor(), + borderRadius: BorderRadius.circular(4), + ), + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 3.w, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 5.w), + GestureDetector( + child: text( + SCAppLocalizations.of(context)!.goldListort, + textColor: + SCGlobalConfig.businessLogicStrategy + .getRechargePageDialogTextColor(), + fontSize: 14.sp, + ), + onTap: () { + Navigator.of(context).pop(); + SCNavigatorUtils.push( + context, + WalletRoute.goldRecord, + replace: false, + ); + }, + ), + SizedBox(height: 5.w), + ], + ), + ), + onTap: () {}, + ), + SizedBox(width: 10.w), + ], + ), + Expanded(child: SizedBox(width: width(1))), + ], + ), + ); + }, + ); + } + + Widget _buildWalletSection() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 15.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 45.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getRechargePageGoldIcon(), + width: 36.w, + height: 36.w, + ), + SizedBox(width: 3.w), + Consumer( + builder: (context, ref, child) { + return text( + ref.myBalance > 1000 + ? "${(ref.myBalance / 1000).toStringAsFixed(2)}k" + : "${ref.myBalance}", + fontSize: 16.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getRechargePageWalletTextColor(), + fontWeight: FontWeight.bold, + ); + }, + ), + ], + ), + ], + ), + ); + } + + Widget _buildMethodSection(MiFaPayManager miFaPayManager) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Recharge methods', + style: TextStyle( + fontSize: 15.sp, + color: _accentGoldColor, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 10.w), + ..._methodOptions.map( + (RechargeMethodOption option) => + _buildMethodTile(option, miFaPayManager), + ), + ], + ); + } + + Widget _buildMethodTile( + RechargeMethodOption option, + MiFaPayManager miFaPayManager, + ) { + final bool isSelected = _selectedMethodType == option.type; + String trailingText = ''; + if (option.type == RechargeMethodType.miFaPay && + miFaPayManager.selectedCommodity != null) { + trailingText = _formatMiFaPriceLabel(miFaPayManager.selectedCommodity!); + } + + return Padding( + padding: EdgeInsets.only(bottom: 10.w), + child: SCDebounceWidget( + onTap: () { + setState(() { + _selectedMethodType = option.type; + }); + if (option.type == RechargeMethodType.miFaPay) { + _openMiFaPaySheet(miFaPayManager); + } + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + width: double.infinity, + height: 58.w, + padding: EdgeInsets.symmetric(horizontal: 14.w), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: + isSelected + ? const [Color(0x42FFFFFF), Color(0x24FFFFFF)] + : const [Color(0x33FFFFFF), Color(0x18FFFFFF)], + begin: AlignmentDirectional.centerStart, + end: AlignmentDirectional.centerEnd, + ), + borderRadius: BorderRadius.circular(10.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10.w, + offset: Offset(0, 3.w), + ), + ], + ), + child: Row( + children: [ + _buildMethodLeading(option), + SizedBox(width: 14.w), + Expanded( + child: Text( + option.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15.sp, + color: _surfaceTextColor, + fontWeight: FontWeight.w600, + ), + ), + ), + if (trailingText.isNotEmpty) + Padding( + padding: EdgeInsetsDirectional.only(end: 8.w), + child: Text( + trailingText, + style: TextStyle( + fontSize: 12.sp, + color: _surfaceSubtleTextColor.withValues(alpha: 0.82), + fontWeight: FontWeight.w600, + ), + ), + ), + Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_off_outlined, + size: 18.w, + color: + isSelected + ? _accentGoldColor + : _surfaceSubtleTextColor.withValues(alpha: 0.45), + ), + ], + ), + ), + ), + ); + } + + Widget _buildMethodLeading(RechargeMethodOption option) { + if (option.assetIconPath != null) { + return Image.asset( + option.assetIconPath!, + width: 28.w, + height: 28.w, + fit: BoxFit.contain, + ); + } + return Icon( + option.iconData ?? CupertinoIcons.creditcard, + size: 24.w, + color: _surfaceTextColor, + ); + } + + Widget _buildNativeProductList() { + return Platform.isAndroid + ? Consumer( + builder: (context, ref, child) { + return ref.products.isNotEmpty + ? ListView.separated( + itemCount: ref.products.length, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + return _buildGoogleProductItem( + ref.products[index], + ref, + index, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(height: 10.w); + }, + ) + : _buildRechargeEmptyState(); + }, + ) + : Consumer( + builder: (context, ref, child) { + return ref.products.isNotEmpty + ? ListView.separated( + itemCount: ref.products.length, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + return _buildAppleProductItem( + ref.products[index], + ref, + index, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(height: 10.w); + }, + ) + : _buildRechargeEmptyState(); + }, + ); + } + + Widget _buildMiFaPayContent(MiFaPayManager miFaPayManager) { + if (miFaPayManager.isLoading && miFaPayManager.commodities.isEmpty) { + return Center(child: CupertinoActivityIndicator(radius: 14.w)); + } + + if (miFaPayManager.errorMessage.isNotEmpty && + miFaPayManager.commodities.isEmpty) { + return _buildMiFaPayStatusCard( + title: 'MiFaPay is temporarily unavailable', + detail: miFaPayManager.errorMessage, + actionText: 'Retry', + onTap: () { + miFaPayManager.reload(); + }, + ); + } + + if (miFaPayManager.commodities.isEmpty) { + return _buildMiFaPayStatusCard( + title: 'No MiFaPay products available', + detail: 'Please try again later.', + ); + } + + final SCMiFaPayCommodityItemRes? commodity = + miFaPayManager.selectedCommodity; + final SCMiFaPayChannelRes? channel = miFaPayManager.selectedChannel; + final SCMiFaPayCountryRes? country = miFaPayManager.selectedCountry; + + return _buildMiFaPayStatusCard( + title: + commodity == null + ? 'Tap MiFaPay to choose an amount' + : 'Selected ${_formatMiFaPriceLabel(commodity)}', + detail: + commodity == null + ? 'Choose amount and payment channel in the bottom sheet.' + : '${_formatWholeNumber(_parseWholeNumber(commodity.content))} +${_formatWholeNumber(_parseWholeNumber(commodity.awardContent))}', + actionText: + channel == null + ? null + : '${channel.channelName ?? ''}${country?.alphaTwo?.isNotEmpty == true ? ' ${country!.alphaTwo}' : ''}', + ); + } + + Widget _buildMiFaPayStatusCard({ + required String title, + required String detail, + String? actionText, + VoidCallback? onTap, + }) { + return Center( + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 16.w), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0x33FFFFFF), Color(0x18FFFFFF)], + begin: AlignmentDirectional.topStart, + end: AlignmentDirectional.bottomEnd, + ), + borderRadius: BorderRadius.circular(10.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10.w, + offset: Offset(0, 3.w), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + color: _surfaceTextColor, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 8.w), + Text( + detail, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.sp, + color: _surfaceSubtleTextColor, + fontWeight: FontWeight.w500, + ), + ), + if ((actionText ?? '').isNotEmpty) ...[ + SizedBox(height: 10.w), + GestureDetector( + onTap: onTap, + child: Text( + actionText!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.sp, + color: + onTap != null + ? _accentGoldColor + : _surfaceSubtleTextColor.withValues(alpha: 0.82), + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ], + ), + ), + ); + } + + Widget _buildRechargeEmptyState() { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + 'sc_images/general/sc_icon_loading.png', + width: 96.w, + fit: BoxFit.fitWidth, + color: Colors.white.withValues(alpha: 0.72), + ), + SizedBox(height: 10.w), + Text( + SCAppLocalizations.of(context)!.noData, + style: TextStyle( + fontSize: 13.sp, + color: _accentGoldColor.withValues(alpha: 0.9), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildFooterButton({ + required String title, + required VoidCallback onTap, + }) { + return SCDebounceWidget( + onTap: onTap, + child: Container( + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 60.w), + height: 42.w, + decoration: BoxDecoration( + color: + SCGlobalConfig.businessLogicStrategy + .getRechargePageButtonBackgroundColor(), + borderRadius: BorderRadius.all(Radius.circular(height(8))), + ), + child: text( + title, + textColor: + SCGlobalConfig.businessLogicStrategy + .getRechargePageButtonTextColor(), + fontSize: 16.sp, + ), + ), + ); + } + + void _processNativePurchase() { + if (Platform.isAndroid) { + Provider.of( + context, + listen: false, + ).processPurchase(); + } else if (Platform.isIOS) { + Provider.of( + context, + listen: false, + ).processPurchase(); + } + } + + String _buildMiFaPayPrimaryTitle(MiFaPayManager miFaPayManager) { + if (miFaPayManager.isCreatingOrder) { + return 'Processing...'; + } + if (miFaPayManager.isLoading && miFaPayManager.commodities.isEmpty) { + return 'Loading...'; + } + if (miFaPayManager.errorMessage.isNotEmpty && + miFaPayManager.commodities.isEmpty) { + return 'Retry'; + } + return miFaPayManager.canCreateRecharge ? 'Pay now' : 'Choose amount'; + } + + void _handleMiFaPayPrimaryAction(MiFaPayManager miFaPayManager) { + if (miFaPayManager.isCreatingOrder) { + return; + } + if (miFaPayManager.errorMessage.isNotEmpty && + miFaPayManager.commodities.isEmpty) { + miFaPayManager.reload(); + return; + } + if (miFaPayManager.canCreateRecharge) { + miFaPayManager.createRecharge(context); + return; + } + _openMiFaPaySheet(miFaPayManager); + } + + void _openMiFaPaySheet(MiFaPayManager miFaPayManager) { + if (miFaPayManager.isLoading && miFaPayManager.commodities.isEmpty) { + SCTts.show('MiFaPay products are loading'); + return; + } + + final List packages = _buildMiFaPackages( + miFaPayManager.commodities, + ); + final List channels = _buildMiFaChannels( + miFaPayManager.channels, + ); + + if (packages.isEmpty || channels.isEmpty) { + if (miFaPayManager.errorMessage.isNotEmpty) { + SCTts.show(miFaPayManager.errorMessage); + } else { + SCTts.show('No MiFaPay product is available now'); + } + return; + } + + showBottomInBottomDialog( + context, + RechargeMethodBottomSheet( + method: _methodOptions.firstWhere( + (RechargeMethodOption item) => + item.type == RechargeMethodType.miFaPay, + ), + packages: packages, + channels: channels, + initialPackageId: miFaPayManager.selectedCommodity?.id, + initialChannelCode: miFaPayManager.selectedChannel?.channelCode, + onConfirm: ( + RechargePackageOption package, + RechargeChannelOption channel, + ) { + setState(() { + _selectedMethodType = RechargeMethodType.miFaPay; + }); + miFaPayManager.chooseCommodityById(package.id); + miFaPayManager.chooseChannelByCode(channel.code); + }, + ), + barrierColor: Colors.black.withValues(alpha: 0.35), + ); + } + + List _buildMiFaPackages( + List commodities, + ) { + return commodities + .where((SCMiFaPayCommodityItemRes item) => (item.id ?? '').isNotEmpty) + .map((SCMiFaPayCommodityItemRes item) { + final int coins = _parseWholeNumber(item.content); + final int bonusCoins = _parseWholeNumber(item.awardContent); + return RechargePackageOption( + id: item.id ?? '', + coins: coins, + bonusCoins: bonusCoins, + priceLabel: _formatMiFaPriceLabel(item), + badge: _buildMiFaBadge(coins, bonusCoins), + ); + }) + .toList(growable: false); + } + + List _buildMiFaChannels( + List channels, + ) { + return channels + .where( + (SCMiFaPayChannelRes item) => (item.channelCode ?? '').isNotEmpty, + ) + .map((SCMiFaPayChannelRes item) { + return RechargeChannelOption( + code: item.channelCode ?? '', + name: item.channelName ?? '', + typeLabel: _buildChannelTypeLabel(item), + ); + }) + .toList(growable: false); + } + + String _buildMiFaBadge(int coins, int bonusCoins) { + if (coins <= 0 || bonusCoins <= 0) { + return ''; + } + final double percent = bonusCoins * 100 / coins; + return '+${percent.toStringAsFixed(percent >= 10 ? 0 : 1)}%'; + } + + String _buildChannelTypeLabel(SCMiFaPayChannelRes channel) { + final List parts = []; + if ((channel.channelType ?? '').isNotEmpty) { + parts.add(channel.channelType!); + } + if ((channel.factoryChannel ?? '').isNotEmpty) { + parts.add(channel.factoryChannel!); + } + return parts.join(' '); + } + + String _formatMiFaPriceLabel(SCMiFaPayCommodityItemRes item) { + final String currency = item.currency ?? 'USD'; + final num amount = item.amount ?? item.amountUsd ?? 0; + final String formattedAmount = + amount % 1 == 0 ? amount.toInt().toString() : amount.toString(); + return '$currency $formattedAmount'; + } + + int _parseWholeNumber(String? value) { + if ((value ?? '').isEmpty) { + return 0; + } + return num.tryParse(value!)?.toInt() ?? 0; + } + + String _formatWholeNumber(num value) { + final String raw = value.toInt().toString(); + final StringBuffer buffer = StringBuffer(); + for (int i = 0; i < raw.length; i++) { + final int position = raw.length - i; + buffer.write(raw[i]); + if (position > 1 && position % 3 == 1) { + buffer.write(','); + } + } + return buffer.toString(); + } + + Widget _buildGoogleProductItem( + SelecteProductConfig productConfig, + AndroidPaymentProcessor ref, + int index, + ) { + return GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.w), + gradient: LinearGradient( + colors: + productConfig.isSelecte + ? const [Color(0x42FFFFFF), Color(0x24FFFFFF)] + : const [Color(0x33FFFFFF), Color(0x18FFFFFF)], + begin: AlignmentDirectional.centerStart, + end: AlignmentDirectional.centerEnd, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10.w, + offset: Offset(0, 3.w), + ), + ], + ), + child: Row( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getRechargePageGoldIconByIndex(index), + width: 34.w, + height: 34.w, + ), + SizedBox(width: 5.w), + text( + "${ref.productMap[productConfig.produc.id]?.obtainCandy ?? 0}", + fontSize: 12.sp, + textColor: _surfaceTextColor, + ), + const Spacer(), + text( + productConfig.produc.price, + textColor: _surfaceSubtleTextColor, + fontWeight: FontWeight.w600, + fontSize: 12.sp, + ), + ], + ), + ), + onTap: () { + ref.chooseProductConfig(index); + }, + ); + } + + Widget _buildAppleProductItem( + SelecteProductConfig productConfig, + IOSPaymentProcessor ref, + int index, + ) { + return GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.w), + gradient: LinearGradient( + colors: + productConfig.isSelecte + ? const [Color(0x42FFFFFF), Color(0x24FFFFFF)] + : const [Color(0x33FFFFFF), Color(0x18FFFFFF)], + begin: AlignmentDirectional.centerStart, + end: AlignmentDirectional.centerEnd, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10.w, + offset: Offset(0, 3.w), + ), + ], + ), + child: Row( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getRechargePageGoldIconByIndex(index), + width: 34.w, + height: 34.w, + ), + SizedBox(width: 5.w), + text( + "${ref.productMap[productConfig.produc.id]?.obtainCandy ?? 0}", + fontSize: 12.sp, + textColor: _surfaceTextColor, + ), + const Spacer(), + text( + productConfig.produc.price, + textColor: _surfaceSubtleTextColor, + fontWeight: FontWeight.w600, + fontSize: 12.sp, + ), + ], + ), + ), + onTap: () { + ref.chooseProductConfig(index); + }, + ); + } +} + +class SelecteProductConfig { + ProductDetails produc; + bool isSelecte = false; + + SelecteProductConfig(this.produc, this.isSelecte); +} diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index bbad618..fcd2e93 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -1,6 +1,8 @@ -import 'package:agora_rtc_engine/agora_rtc_engine.dart'; -import 'package:fluro/fluro.dart'; -import 'package:flutter/cupertino.dart'; +import 'dart:async'; + +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:fluro/fluro.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; @@ -43,9 +45,17 @@ import '../../ui_kit/components/sc_float_ichart.dart'; typedef OnSoundVoiceChange = Function(num index, int volum); typedef RtcProvider = RealTimeCommunicationManager; -class RealTimeCommunicationManager extends ChangeNotifier { - bool needUpDataUserInfo = false; - bool _roomVisualEffectsEnabled = false; +class RealTimeCommunicationManager extends ChangeNotifier { + static const Duration _micListPollingInterval = Duration(seconds: 2); + static const Duration _onlineUsersPollingInterval = Duration(seconds: 3); + + bool needUpDataUserInfo = false; + bool _roomVisualEffectsEnabled = false; + bool _isExitingCurrentVoiceRoomSession = false; + Timer? _micListPollingTimer; + Timer? _onlineUsersPollingTimer; + bool _isRefreshingMicList = false; + bool _isRefreshingOnlineUsers = false; ///当前所在房间 JoinRoomRes? currenRoom; @@ -91,6 +101,9 @@ class RealTimeCommunicationManager extends ChangeNotifier { bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled; + bool get isExitingCurrentVoiceRoomSession => + _isExitingCurrentVoiceRoomSession; + bool get shouldShowRoomVisualEffects => currenRoom != null && _roomVisualEffectsEnabled; @@ -102,6 +115,186 @@ class RealTimeCommunicationManager extends ChangeNotifier { notifyListeners(); } + void _setExitingCurrentVoiceRoomSession(bool enabled, {bool notify = true}) { + if (_isExitingCurrentVoiceRoomSession == enabled) { + return; + } + _isExitingCurrentVoiceRoomSession = enabled; + if (notify) { + notifyListeners(); + } + } + + void _startRoomStatePolling() { + _stopRoomStatePolling(); + if ((currenRoom?.roomProfile?.roomProfile?.id ?? "").isEmpty) { + return; + } + _micListPollingTimer = Timer.periodic(_micListPollingInterval, (_) { + retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {}); + }); + _onlineUsersPollingTimer = Timer.periodic(_onlineUsersPollingInterval, (_) { + fetchOnlineUsersList(notifyIfUnchanged: false).catchError((_) {}); + }); + } + + void _stopRoomStatePolling() { + _micListPollingTimer?.cancel(); + _onlineUsersPollingTimer?.cancel(); + _micListPollingTimer = null; + _onlineUsersPollingTimer = null; + _isRefreshingMicList = false; + _isRefreshingOnlineUsers = false; + } + + bool _sameOnlineUsers( + List previous, + List next, + ) { + if (previous.length != next.length) { + return false; + } + for (int i = 0; i < previous.length; i++) { + if (!_sameUserProfile(previous[i], next[i])) { + return false; + } + } + return true; + } + + bool _sameUserProfile( + SocialChatUserProfile? previous, + SocialChatUserProfile? next, + ) { + if (identical(previous, next)) { + return true; + } + if (previous == null || next == null) { + return previous == next; + } + return previous.id == next.id && + previous.account == next.account && + previous.userAvatar == next.userAvatar && + previous.userNickname == next.userNickname && + previous.userSex == next.userSex && + previous.roles == next.roles && + previous.heartbeatVal == next.heartbeatVal; + } + + bool _sameMicMaps(Map previous, Map next) { + if (previous.length != next.length) { + return false; + } + for (final entry in next.entries) { + if (!_sameMic(previous[entry.key], entry.value)) { + return false; + } + } + return true; + } + + bool _sameMic(MicRes? previous, MicRes? next) { + if (identical(previous, next)) { + return true; + } + if (previous == null || next == null) { + return previous == next; + } + return previous.micIndex == next.micIndex && + previous.micLock == next.micLock && + previous.micMute == next.micMute && + _sameUserProfile(previous.user, next.user); + } + + void _refreshManagerUsers(List users) { + managerUsers + ..clear() + ..addAll( + users.where((user) => user.roles == SCRoomRolesType.ADMIN.name), + ); + } + + Map _buildMicMap(List roomWheatList) { + final previousMap = roomWheatMap; + final nextMap = {}; + for (final roomWheat in roomWheatList) { + final micIndex = roomWheat.micIndex; + if (micIndex == null) { + continue; + } + final previousMic = previousMap[micIndex]; + final shouldPreserveTransientState = + previousMic?.user?.id == roomWheat.user?.id && + previousMic?.user?.id != null; + nextMap[micIndex] = + shouldPreserveTransientState + ? roomWheat.copyWith( + emojiPath: + (roomWheat.emojiPath ?? "").isNotEmpty + ? roomWheat.emojiPath + : previousMic?.emojiPath, + type: + (roomWheat.type ?? "").isNotEmpty + ? roomWheat.type + : previousMic?.type, + number: + (roomWheat.number ?? "").isNotEmpty + ? roomWheat.number + : previousMic?.number, + ) + : roomWheat; + } + return nextMap; + } + + void _syncSelfMicRuntimeState() { + final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; + if ((currentUserId ?? "").isEmpty) { + return; + } + + MicRes? currentUserMic; + for (final mic in roomWheatMap.values) { + if (mic.user?.id == currentUserId) { + currentUserMic = mic; + break; + } + } + + if (currentUserMic == null) { + SCHeartbeatUtils.cancelAnchorTimer(); + engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + engine?.muteLocalAudioStream(true); + return; + } + + SCHeartbeatUtils.scheduleAnchorHeartbeat( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + + if ((currentUserMic.micMute ?? false) || isMic) { + engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + engine?.muteLocalAudioStream(true); + return; + } + + adjustRecordingSignalVolume(100); + engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + engine?.muteLocalAudioStream(false); + } + + void _applyMicSnapshot( + Map nextMap, { + bool notifyIfUnchanged = true, + }) { + final changed = !_sameMicMaps(roomWheatMap, nextMap); + roomWheatMap = nextMap; + _syncSelfMicRuntimeState(); + if (changed || notifyIfUnchanged) { + notifyListeners(); + } + } + Future joinAgoraVoiceChannel() async { try { engine = await _initAgoraRtcEngine(); @@ -327,7 +520,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { SCTts.show('Microphone permission is denied.'); throw ArgumentError('Microphone permission is denied.'); } - if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { + if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { ///最小化进入房间,或者进入的是同一个房间 final loaded = await loadRoomInfo( currenRoom?.roomProfile?.roomProfile?.id ?? "", @@ -338,13 +531,15 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (!context.mounted) { return; } - Provider.of( - context, - listen: false, - ).fetchUserProfileData(); - retrieveMicrophoneList(); - setRoomVisualEffectsEnabled(true); - SCFloatIchart().remove(); + Provider.of( + context, + listen: false, + ).fetchUserProfileData(); + retrieveMicrophoneList(); + fetchOnlineUsersList(); + _startRoomStatePolling(); + setRoomVisualEffectsEnabled(true); + SCFloatIchart().remove(); SCNavigatorUtils.push( context, '${VoiceRoomRoute.voiceRoom}?id=$roomId', @@ -450,12 +645,14 @@ class RealTimeCommunicationManager extends ChangeNotifier { type: SCRoomMsgType.systemTips, ), ); - - ///获取麦位 - retrieveMicrophoneList(); - Provider.of( - context!, - listen: false, + + ///获取麦位 + retrieveMicrophoneList(); + fetchOnlineUsersList(); + _startRoomStatePolling(); + Provider.of( + context!, + listen: false, ).fetchContributionLevelData( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); @@ -509,48 +706,60 @@ class RealTimeCommunicationManager extends ChangeNotifier { notifyListeners(); } - ///获取在线用户 - Future fetchOnlineUsersList() async { - onlineUsers = await SCChatRoomRepository().roomOnlineUsers( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - ); - managerUsers.clear(); - for (var user in onlineUsers) { - if (user.roles == SCRoomRolesType.ADMIN.name) { - managerUsers.add(user); - } - } - notifyListeners(); - } - - Future retrieveMicrophoneList() async { - bool isOnMic = false; - var roomWheatList = await SCChatRoomRepository().micList( - currenRoom!.roomProfile?.roomProfile?.id ?? "", - ); - for (var roomWheat in roomWheatList) { - roomWheatMap[roomWheat.micIndex!] = roomWheat; - if (roomWheat.user != null && - roomWheat.user!.id == - AccountStorage().getCurrentUser()?.userProfile?.id) { - isOnMic = true; - - ///自己在麦上 - SCHeartbeatUtils.scheduleAnchorHeartbeat( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - ); - } - } - notifyListeners(); - SCHeartbeatUtils.scheduleHeartbeat( - SCHeartbeatStatus.VOICE_LIVE.name, - isOnMic, - roomId: currenRoom?.roomProfile?.roomProfile?.id, - ); - Future.delayed(Duration(milliseconds: 1500), () { - fetchOnlineUsersList(); - }); - } + ///获取在线用户 + Future fetchOnlineUsersList({bool notifyIfUnchanged = true}) async { + final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; + if (roomId.isEmpty || _isRefreshingOnlineUsers) { + return; + } + _isRefreshingOnlineUsers = true; + try { + final fetchedUsers = await SCChatRoomRepository().roomOnlineUsers(roomId); + if (roomId != currenRoom?.roomProfile?.roomProfile?.id) { + return; + } + final changed = !_sameOnlineUsers(onlineUsers, fetchedUsers); + onlineUsers = fetchedUsers; + _refreshManagerUsers(onlineUsers); + if (changed || notifyIfUnchanged) { + notifyListeners(); + } + } finally { + _isRefreshingOnlineUsers = false; + } + } + + Future retrieveMicrophoneList({bool notifyIfUnchanged = true}) async { + final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; + if (roomId.isEmpty || _isRefreshingMicList) { + return; + } + _isRefreshingMicList = true; + bool isOnMic = false; + try { + final roomWheatList = await SCChatRoomRepository().micList(roomId); + if (roomId != currenRoom?.roomProfile?.roomProfile?.id) { + return; + } + final nextMap = _buildMicMap(roomWheatList); + for (final roomWheat in nextMap.values) { + if (roomWheat.user != null && + roomWheat.user!.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + isOnMic = true; + break; + } + } + _applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged); + SCHeartbeatUtils.scheduleHeartbeat( + SCHeartbeatStatus.VOICE_LIVE.name, + isOnMic, + roomId: currenRoom?.roomProfile?.roomProfile?.id, + ); + } finally { + _isRefreshingMicList = false; + } + } void fetchRoomTaskClaimableCount() { SCChatRoomRepository() @@ -576,9 +785,11 @@ class RealTimeCommunicationManager extends ChangeNotifier { return result; } - Future exitCurrentVoiceRoomSession(bool isLogout) async { - try { - rtmProvider?.msgAllListener = null; + Future exitCurrentVoiceRoomSession(bool isLogout) async { + _setExitingCurrentVoiceRoomSession(true); + _stopRoomStatePolling(); + try { + rtmProvider?.msgAllListener = null; rtmProvider?.msgChatListener = null; rtmProvider?.msgGiftListener = null; SCLoadingManager.show(context: context); @@ -611,10 +822,11 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } - Future resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async { - rtmProvider ??= fallbackRtmProvider; - try { - SCHeartbeatUtils.cancelTimer(); + Future resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async { + rtmProvider ??= fallbackRtmProvider; + _stopRoomStatePolling(); + try { + SCHeartbeatUtils.cancelTimer(); SCHeartbeatUtils.cancelAnchorTimer(); final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; if (groupId.isNotEmpty) { @@ -634,14 +846,16 @@ class RealTimeCommunicationManager extends ChangeNotifier { } ///清空列表数据 - void _clearData() { - roomRocketStatus = null; + void _clearData() { + _stopRoomStatePolling(); + roomRocketStatus = null; rtmProvider ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = null; - roomWheatMap.clear(); - onlineUsers.clear(); - needUpDataUserInfo = false; + roomWheatMap.clear(); + onlineUsers.clear(); + managerUsers.clear(); + needUpDataUserInfo = false; SCRoomUtils.roomUsersMap.clear(); roomIsMute = false; rtmProvider?.roomAllMsgList.clear(); @@ -649,6 +863,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { redPacketList.clear(); currenRoom = null; _roomVisualEffectsEnabled = false; + _isExitingCurrentVoiceRoomSession = false; isMic = true; isMusicPlaying = false; DataPersistence.setLastTimeRoomId(""); @@ -715,10 +930,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } - bool _handleDirectSeatInteraction( - num index, { - SocialChatUserProfile? clickUser, - }) { + bool _handleDirectSeatInteraction( + num index, { + SocialChatUserProfile? clickUser, + }) { final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; final isRoomAdmin = isFz() || isGL(); @@ -742,20 +957,27 @@ class RealTimeCommunicationManager extends ChangeNotifier { return true; } return false; - } - - if (seatUser.id == currentUserId) { - _openRoomUserInfoCard(seatUser.id); - return true; - } + } + + if (seatUser.id == currentUserId) { + if (isRoomAdmin) { + return false; + } + _openRoomUserInfoCard(seatUser.id); + return true; + } if (!isRoomAdmin && (seatUser.id ?? '').isNotEmpty) { _openRoomUserInfoCard(seatUser.id); return true; } - return false; - } + return false; + } + + void _refreshMicListSilently() { + retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {}); + } void _openRoomUserInfoCard(String? userId) { final normalizedUserId = (userId ?? '').trim(); @@ -778,8 +1000,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { print('删除监听:${_onSoundVoiceChangeList.length}'); } - void shangMai(num index, {String? eventType, String? inviterId}) async { - var myUser = AccountStorage().getCurrentUser()?.userProfile; + void shangMai(num index, {String? eventType, String? inviterId}) async { + var myUser = AccountStorage().getCurrentUser()?.userProfile; if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) { } else { if (isTourists()) { @@ -800,62 +1022,68 @@ class RealTimeCommunicationManager extends ChangeNotifier { /// 设置成主播角色 engine?.renewToken(micGoUpRes.roomToken ?? ""); - if (!micGoUpRes.micMute!) { - if (!isMic) { - engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); - adjustRecordingSignalVolume(100); - await engine?.muteLocalAudioStream(false); + if (!micGoUpRes.micMute!) { + if (!isMic) { + engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + adjustRecordingSignalVolume(100); + await engine?.muteLocalAudioStream(false); } } else { engine?.setClientRole(role: ClientRoleType.clientRoleAudience); await engine?.muteLocalAudioStream(true); } - SCHeartbeatUtils.scheduleAnchorHeartbeat( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - ); - var us = roomWheatMap[index]; - us?.copyWith( - user: SocialChatUserProfile( - id: myUser?.id, - account: myUser?.account, - userAvatar: myUser?.userAvatar, - userNickname: myUser?.userNickname, - userSex: myUser?.userSex, - ), - ); - roomWheatMap[index] = us!; - if (us.micMute!) { - ///房主上麦自动解禁麦位 - if (isFz()) { - jieJinMai(index); - } - } - - notifyListeners(); - } catch (ex) { - SCTts.show('Failed to put on the microphone, $ex'); - } - } - - xiaMai(num index) async { - SCChatRoomRepository() - .micGoDown(currenRoom?.roomProfile?.roomProfile?.id ?? "", index) - .whenComplete(() { - //自己 - if (roomWheatMap[index]?.user?.id == - AccountStorage().getCurrentUser()?.userProfile?.id) { - isMic = true; - engine?.muteLocalAudioStream(true); - } - SCHeartbeatUtils.cancelAnchorTimer(); - - /// 设置成主持人角色 - engine?.renewToken(""); - engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - roomWheatMap[index]?.setUser = null; - notifyListeners(); - }); - } + SCHeartbeatUtils.scheduleAnchorHeartbeat( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + final currentSeat = roomWheatMap[index]; + if (currentSeat != null && myUser != null) { + roomWheatMap[index] = currentSeat.copyWith( + user: myUser.copyWith(roles: currenRoom?.entrants?.roles), + micMute: micGoUpRes.micMute, + roomToken: micGoUpRes.roomToken, + ); + } + if (roomWheatMap[index]?.micMute ?? false) { + ///房主上麦自动解禁麦位 + if (isFz()) { + await jieJinMai(index); + } + } + + notifyListeners(); + _refreshMicListSilently(); + } catch (ex) { + SCTts.show('Failed to put on the microphone, $ex'); + } + } + + xiaMai(num index) async { + try { + await SCChatRoomRepository().micGoDown( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + ); + + if (roomWheatMap[index]?.user?.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + isMic = true; + engine?.muteLocalAudioStream(true); + } + SCHeartbeatUtils.cancelAnchorTimer(); + + /// 设置成主持人角色 + engine?.renewToken(""); + engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + final currentSeat = roomWheatMap[index]; + if (currentSeat != null) { + roomWheatMap[index] = currentSeat.copyWith(user: null); + } + notifyListeners(); + _refreshMicListSilently(); + } catch (ex) { + SCTts.show('Failed to leave the microphone, $ex'); + } + } ///踢人下麦 killXiaMai(String userId) { @@ -949,37 +1177,20 @@ class RealTimeCommunicationManager extends ChangeNotifier { } ///麦位变动 - void micChange(List? mics) { - if (mics == null || mics.isEmpty) { - return; - } - roomWheatMap.clear(); - for (var mic in mics) { - roomWheatMap[mic.micIndex!] = mic; - if (mic.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { - if (mic.micMute!) { - ///麦克风静音 - engine?.muteLocalAudioStream(true); - } else { - // if (!isMic) { - // engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); - // } - // engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); - ///这个用户需要禁麦/解禁麦克风 - engine?.muteLocalAudioStream(false); - } - } - } - notifyListeners(); - - ///判断自己是不是在麦上 - num index = userOnMaiInIndex( - AccountStorage().getCurrentUser()?.userProfile?.id ?? "", - ); - if (index == -1) { - engine?.muteLocalAudioStream(true); - } - } + void micChange(List? mics) { + if (mics == null || mics.isEmpty) { + return; + } + _applyMicSnapshot(_buildMicMap(mics)); + final isOnMic = + userOnMaiInIndex(AccountStorage().getCurrentUser()?.userProfile?.id ?? "") > + -1; + SCHeartbeatUtils.scheduleHeartbeat( + SCHeartbeatStatus.VOICE_LIVE.name, + isOnMic, + roomId: currenRoom?.roomProfile?.roomProfile?.id, + ); + } ///找一个空麦位 -1表示没有空位 num findWheat() { @@ -1044,49 +1255,66 @@ class RealTimeCommunicationManager extends ChangeNotifier { } ///解锁麦位 - void jieFeng(num index) async { - await SCChatRoomRepository().micLock( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - index, - false, - ); - } - - ///锁麦 - void fengMai(num index) async { - await SCChatRoomRepository().micLock( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - index, - true, - ); - } - - ///静音麦克风 - void jinMai(num index) async { - await SCChatRoomRepository().micMute( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - index, - true, - ); - if (isOnMaiInIndex(index)) { - Provider.of( - context!, - listen: false, - ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - } - var mic = roomWheatMap[index]; - if (mic != null) { - roomWheatMap[index] = mic.copyWith(micMute: true); - notifyListeners(); - } - } - - ///解除静音麦克风 - void jieJinMai(num index) async { - await SCChatRoomRepository().micMute( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - index, - false, + Future jieFeng(num index) async { + await SCChatRoomRepository().micLock( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + false, + ); + final mic = roomWheatMap[index]; + if (mic != null) { + roomWheatMap[index] = mic.copyWith(micLock: false); + notifyListeners(); + } + _refreshMicListSilently(); + } + + ///锁麦 + Future fengMai(num index) async { + await SCChatRoomRepository().micLock( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + true, + ); + final mic = roomWheatMap[index]; + if (mic != null) { + roomWheatMap[index] = mic.copyWith(micLock: true); + notifyListeners(); + } + _refreshMicListSilently(); + } + + ///静音麦克风 + Future jinMai(num index) async { + await SCChatRoomRepository().micMute( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + true, + ); + if (isOnMaiInIndex(index)) { + Provider.of( + context!, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + Provider.of( + context!, + listen: false, + ).engine?.muteLocalAudioStream(true); + } + var mic = roomWheatMap[index]; + if (mic != null) { + roomWheatMap[index] = mic.copyWith(micMute: true); + notifyListeners(); + } + _refreshMicListSilently(); + } + + ///解除静音麦克风 + Future jieJinMai(num index) async { + await SCChatRoomRepository().micMute( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + false, ); if (isOnMaiInIndex(index)) { if (!Provider.of(context!, listen: false).isMic) { @@ -1105,11 +1333,12 @@ class RealTimeCommunicationManager extends ChangeNotifier { } var mic = roomWheatMap[index]; - if (mic != null) { - roomWheatMap[index] = mic.copyWith(micMute: false); - notifyListeners(); - } - } + if (mic != null) { + roomWheatMap[index] = mic.copyWith(micMute: false); + notifyListeners(); + } + _refreshMicListSilently(); + } void addOnlineUser(String groupId, SocialChatUserProfile user) { if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { @@ -1122,11 +1351,12 @@ class RealTimeCommunicationManager extends ChangeNotifier { break; } } - if (!isExtOnlineList) { - Provider.of(context!, listen: false).onlineUsers.add(user); - notifyListeners(); - } - } + if (!isExtOnlineList) { + Provider.of(context!, listen: false).onlineUsers.add(user); + _refreshManagerUsers(onlineUsers); + notifyListeners(); + } + } void removOnlineUser(String groupId, String userId) { if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { @@ -1139,14 +1369,15 @@ class RealTimeCommunicationManager extends ChangeNotifier { break; } } - if (isExtOnlineUser != null) { - Provider.of( - context!, - listen: false, - ).onlineUsers.remove(isExtOnlineUser); - notifyListeners(); - } - } + if (isExtOnlineUser != null) { + Provider.of( + context!, + listen: false, + ).onlineUsers.remove(isExtOnlineUser); + _refreshManagerUsers(onlineUsers); + notifyListeners(); + } + } void starPlayEmoji(Msg msg) { if (msg.number! > -1) { diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index ec19d27..ad792a0 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -71,6 +71,7 @@ typedef RtmProvider = RealTimeMessagingManager; class RealTimeMessagingManager extends ChangeNotifier { static const int _giftComboMergeWindowMs = 3000; + static const int _maxLuckGiftPushQueueLength = 12; BuildContext? context; @@ -257,13 +258,26 @@ class RealTimeMessagingManager extends ChangeNotifier { ); TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( - onMemberEnter: - (String groupID, List memberList) {}, - onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { - Provider.of( + onMemberEnter: (String groupID, List memberList) { + final rtcProvider = Provider.of( context, listen: false, - ).removOnlineUser(groupID, member.userID!); + ); + if (groupID == + rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { + rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); + } + }, + onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { + final rtcProvider = Provider.of( + context, + listen: false, + ); + rtcProvider.removOnlineUser(groupID, member.userID!); + if (groupID == + rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { + rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); + } }, onMemberKicked: ( String groupID, @@ -275,13 +289,13 @@ class RealTimeMessagingManager extends ChangeNotifier { if (memberList.first.userID == AccountStorage().getCurrentUser()?.userProfile?.id) { Provider.of( - context!, + context, listen: false, ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); ///退出房间 Provider.of( - context!, + context, listen: false, ).exitCurrentVoiceRoomSession(false).whenComplete(() { SCRoomUtils.closeAllDialogs(); @@ -1314,7 +1328,10 @@ class RealTimeMessagingManager extends ChangeNotifier { listen: false, ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); } - // Provider.of(context!, listen: false).getMicList(); + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(notifyIfUnchanged: false); return; } @@ -1353,11 +1370,19 @@ class RealTimeMessagingManager extends ChangeNotifier { context!, listen: false, ).micChange(BroadCastMicChangePush.fromJson(data).data?.mics); + } else if (msg.type == SCRoomMsgType.shangMai || + msg.type == SCRoomMsgType.xiaMai || + msg.type == SCRoomMsgType.fengMai || + msg.type == SCRoomMsgType.jieFeng) { + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(notifyIfUnchanged: false); } else if (msg.type == SCRoomMsgType.refreshOnlineUser) { Provider.of( context!, listen: false, - ).fetchOnlineUsersList(); + ).fetchOnlineUsersList(notifyIfUnchanged: false); } else if (msg.type == SCRoomMsgType.gameLuckyGift) { var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); _giftFxLog( @@ -1669,6 +1694,9 @@ class RealTimeMessagingManager extends ChangeNotifier { void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { if (SCGlobalConfig.isLuckGiftSpecialEffects) { + while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) { + _luckGiftPushQueue.removeFirst(); + } _luckGiftPushQueue.add(broadCastRes); playLuckGiftBackCoins(); } diff --git a/lib/services/gift/gift_animation_manager.dart b/lib/services/gift/gift_animation_manager.dart index cd4c7ff..01810cf 100644 --- a/lib/services/gift/gift_animation_manager.dart +++ b/lib/services/gift/gift_animation_manager.dart @@ -5,6 +5,8 @@ import 'package:flutter/cupertino.dart'; import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; class GiftAnimationManager extends ChangeNotifier { + static const int _maxPendingAnimations = 24; + Queue pendingAnimationsQueue = Queue(); List animationControllerList = []; @@ -15,10 +17,77 @@ class GiftAnimationManager extends ChangeNotifier { animationControllerList.length >= giftMap.length; void enqueueGiftAnimation(LGiftModel giftModel) { + if (_mergeIntoActiveAnimation(giftModel)) { + return; + } + if (_mergeIntoPendingAnimation(giftModel)) { + return; + } + _trimPendingAnimations(); pendingAnimationsQueue.add(giftModel); proceedToNextAnimation(); } + void _trimPendingAnimations() { + while (pendingAnimationsQueue.length >= _maxPendingAnimations) { + pendingAnimationsQueue.removeFirst(); + } + } + + bool _mergeIntoActiveAnimation(LGiftModel incoming) { + for (final entry in giftMap.entries) { + final current = entry.value; + if (current == null || current.labelId != incoming.labelId) { + continue; + } + _mergeGiftModel(target: current, incoming: incoming); + notifyListeners(); + if (animationControllerList.length > entry.key) { + animationControllerList[entry.key].controller.forward(from: 0.45); + } + return true; + } + return false; + } + + bool _mergeIntoPendingAnimation(LGiftModel incoming) { + for (final pending in pendingAnimationsQueue) { + if (pending.labelId != incoming.labelId) { + continue; + } + _mergeGiftModel(target: pending, incoming: incoming); + return true; + } + return false; + } + + void _mergeGiftModel({ + required LGiftModel target, + required LGiftModel incoming, + }) { + target.giftCount = (target.giftCount) + (incoming.giftCount); + if (target.sendUserName.isEmpty) { + target.sendUserName = incoming.sendUserName; + } + if (target.sendToUserName.isEmpty) { + target.sendToUserName = incoming.sendToUserName; + } + if (target.sendUserPic.isEmpty) { + target.sendUserPic = incoming.sendUserPic; + } + if (target.giftPic.isEmpty) { + target.giftPic = incoming.giftPic; + } + if (target.giftName.isEmpty) { + target.giftName = incoming.giftName; + } + if (incoming.rewardAmountText.isNotEmpty) { + target.rewardAmountText = incoming.rewardAmountText; + } + target.showLuckyRewardFrame = + target.showLuckyRewardFrame || incoming.showLuckyRewardFrame; + } + ///开始播放 proceedToNextAnimation() { if (pendingAnimationsQueue.isEmpty || !_controllersReady) { @@ -35,28 +104,7 @@ class GiftAnimationManager extends ChangeNotifier { break; } else { if (value.labelId == playGift.labelId) { - playGift.giftCount = value.giftCount + playGift.giftCount; - if (playGift.sendUserName.isEmpty) { - playGift.sendUserName = value.sendUserName; - } - if (playGift.sendToUserName.isEmpty) { - playGift.sendToUserName = value.sendToUserName; - } - if (playGift.sendUserPic.isEmpty) { - playGift.sendUserPic = value.sendUserPic; - } - if (playGift.giftPic.isEmpty) { - playGift.giftPic = value.giftPic; - } - if (playGift.giftName.isEmpty) { - playGift.giftName = value.giftName; - } - if (playGift.rewardAmountText.isEmpty) { - playGift.rewardAmountText = value.rewardAmountText; - } - playGift.showLuckyRewardFrame = - playGift.showLuckyRewardFrame || value.showLuckyRewardFrame; - giftMap[key] = playGift; + _mergeGiftModel(target: value, incoming: playGift); pendingAnimationsQueue.removeFirst(); notifyListeners(); animationControllerList[key].controller.forward(from: 0.45); @@ -80,6 +128,7 @@ class GiftAnimationManager extends ChangeNotifier { for (var element in animationControllerList) { element.controller.dispose(); } + animationControllerList.clear(); } void markAnimationAsFinished(int index) { diff --git a/lib/services/payment/mifa_pay_manager.dart b/lib/services/payment/mifa_pay_manager.dart new file mode 100644 index 0000000..c2755bc --- /dev/null +++ b/lib/services/payment/mifa_pay_manager.dart @@ -0,0 +1,451 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/modules/wallet/recharge/mifa_pay_webview_page.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; + +class MiFaPayManager extends ChangeNotifier { + static const String defaultApplicationId = '2048200000000000701'; + static const String defaultPayCountryId = '2048200000000000401'; + static const String defaultPayCountryName = 'Saudi Arabia'; + static const String defaultPayCountryCode = 'SA'; + + bool _initialized = false; + bool _isLoading = false; + bool _isCreatingOrder = false; + String _errorMessage = ''; + String _applicationId = defaultApplicationId; + + List _countries = []; + SCMiFaPayCountryRes? _selectedCountry; + SCMiFaPayCommodityRes? _commodityRes; + SCMiFaPayCommodityItemRes? _selectedCommodity; + SCMiFaPayChannelRes? _selectedChannel; + + bool get initialized => _initialized; + + bool get isLoading => _isLoading; + + bool get isCreatingOrder => _isCreatingOrder; + + String get errorMessage => _errorMessage; + + String get applicationId => _applicationId; + + List get countries => _countries; + + SCMiFaPayCountryRes? get selectedCountry => _selectedCountry; + + SCMiFaPayCommodityRes? get commodityRes => _commodityRes; + + List get commodities => + _commodityRes?.commodity ?? []; + + List get channels => + _commodityRes?.channels ?? []; + + SCMiFaPayCommodityItemRes? get selectedCommodity => _selectedCommodity; + + SCMiFaPayChannelRes? get selectedChannel => _selectedChannel; + + bool get canCreateRecharge => + _selectedCountry != null && + _selectedCommodity != null && + _selectedChannel != null && + !_isLoading && + !_isCreatingOrder; + + Future initialize({bool force = false}) async { + if (_isLoading) { + return; + } + if (_initialized && !force) { + return; + } + + _isLoading = true; + _errorMessage = ''; + notifyListeners(); + + try { + await _loadCountries(); + await _loadCommodity(); + _initialized = true; + } catch (error) { + _errorMessage = _readErrorMessage(error); + debugPrint('MiFaPay initialize failed: $error'); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Future reload() async { + await initialize(force: true); + } + + void chooseCommodityById(String goodsId) { + if (goodsId.isEmpty) { + return; + } + for (final SCMiFaPayCommodityItemRes item in commodities) { + if (item.id == goodsId) { + _selectedCommodity = item; + notifyListeners(); + return; + } + } + } + + void chooseChannelByCode(String channelCode) { + if (channelCode.isEmpty) { + return; + } + for (final SCMiFaPayChannelRes item in channels) { + if (item.channelCode == channelCode) { + _selectedChannel = item; + notifyListeners(); + return; + } + } + } + + Future createRecharge(BuildContext context) async { + final SCMiFaPayCountryRes? country = _selectedCountry; + final SCMiFaPayCommodityItemRes? commodity = _selectedCommodity; + final SCMiFaPayChannelRes? channel = _selectedChannel; + final String userId = + AccountStorage().getCurrentUser()?.userProfile?.id ?? ""; + final NavigatorState navigator = Navigator.of(context); + final String purchaseSuccessMessage = + SCAppLocalizations.of(context)!.purchaseIsSuccessful; + const String paymentPendingMessage = 'Payment confirmation in progress'; + final Map payload = { + "applicationId": _applicationId, + "goodsId": commodity?.id ?? '', + "payCountryId": country?.id ?? '', + "userId": userId, + "channelCode": channel?.channelCode ?? '', + "newVersion": true, + }; + + if (country?.id?.isNotEmpty != true || + commodity?.id?.isNotEmpty != true || + channel?.channelCode?.isNotEmpty != true) { + SCTts.show('Please select MiFaPay amount and channel first'); + return; + } + if (userId.isEmpty) { + SCTts.show('User id is empty'); + return; + } + if (_isCreatingOrder) { + return; + } + + final SocialChatUserProfileManager profileManager = + Provider.of(context, listen: false); + final double balanceBeforeRecharge = profileManager.myBalance; + + try { + _isCreatingOrder = true; + notifyListeners(); + SCLoadingManager.show(context: context); + _logRequest( + method: 'POST', + path: '/order/web/pay/recharge', + body: payload, + ); + final SCMiFaPayRechargeRes res = await SCConfigRepositoryImp() + .mifaPayRecharge( + applicationId: _applicationId, + goodsId: commodity!.id!, + payCountryId: country!.id!, + userId: userId, + channelCode: channel!.channelCode!, + ); + SCLoadingManager.hide(); + _logResponse(tag: 'recharge', data: res.toJson()); + + if ((res.requestUrl ?? "").isEmpty) { + SCTts.show('MiFaPay requestUrl is empty'); + return; + } + + await navigator.push( + MaterialPageRoute( + builder: + (_) => MiFaPayWebViewPage( + title: 'MiFaPay', + requestUrl: res.requestUrl!, + ), + ), + ); + + final bool rechargeSucceeded = await _waitForRechargeResult( + profileManager, + balanceBeforeRecharge, + ); + SCTts.show( + rechargeSucceeded ? purchaseSuccessMessage : paymentPendingMessage, + ); + } catch (error) { + _logError( + tag: 'recharge', + path: '/order/web/pay/recharge', + error: error, + body: payload, + ); + final String message = _readErrorMessage(error); + SCTts.show(message); + debugPrint('MiFaPay createRecharge failed: $error'); + } finally { + SCLoadingManager.hide(); + _isCreatingOrder = false; + notifyListeners(); + } + } + + Future _loadCountries() async { + List countries = []; + try { + _logRequest(method: 'GET', path: '/order/web/pay/country'); + countries = await SCConfigRepositoryImp().mifaPayCountries(); + _logResponse( + tag: 'country', + data: + countries.map((SCMiFaPayCountryRes item) => item.toJson()).toList(), + ); + } catch (error) { + _logError(tag: 'country', path: '/order/web/pay/country', error: error); + debugPrint('MiFaPay countries fallback: $error'); + } + + if (countries.isEmpty) { + countries = [_fallbackCountry()]; + } + + _countries = countries; + final String currentCountryId = _selectedCountry?.id ?? ''; + _selectedCountry = countries.firstWhere( + (SCMiFaPayCountryRes item) => item.id == currentCountryId, + orElse: () => countries.first, + ); + } + + Future _loadCommodity() async { + final String payCountryId = + _selectedCountry?.id?.isNotEmpty == true + ? _selectedCountry!.id! + : defaultPayCountryId; + final Map queryParams = { + "applicationId": _applicationId, + "payCountryId": payCountryId, + "type": "GOLD", + }; + + try { + _logRequest( + method: 'GET', + path: '/order/web/pay/commodity', + queryParams: queryParams, + ); + final SCMiFaPayCommodityRes res = await SCConfigRepositoryImp() + .mifaPayCommodity( + applicationId: _applicationId, + payCountryId: payCountryId, + ); + _logResponse(tag: 'commodity', data: res.toJson()); + + _commodityRes = res; + _applicationId = res.application?.id ?? _applicationId; + + final List commodityList = commodities; + final String currentGoodsId = _selectedCommodity?.id ?? ''; + _selectedCommodity = + commodityList.isEmpty + ? null + : commodityList.firstWhere( + (SCMiFaPayCommodityItemRes item) => item.id == currentGoodsId, + orElse: () => commodityList.first, + ); + + final List channelList = channels; + final String currentChannelCode = _selectedChannel?.channelCode ?? ''; + _selectedChannel = + channelList.isEmpty + ? null + : channelList.firstWhere( + (SCMiFaPayChannelRes item) => + item.channelCode == currentChannelCode, + orElse: () => channelList.first, + ); + } catch (error) { + _logError( + tag: 'commodity', + path: '/order/web/pay/commodity', + error: error, + queryParams: queryParams, + ); + rethrow; + } + } + + Future _refreshWalletState( + SocialChatUserProfileManager profileManager, + ) async { + try { + final double balance = await SCAccountRepository().balance(); + profileManager.updateBalance(balance); + } catch (error) { + debugPrint('MiFaPay balance refresh failed: $error'); + } + + try { + await profileManager.fetchUserProfileData(loadGuardCount: false); + } catch (error) { + debugPrint('MiFaPay profile refresh failed: $error'); + } + } + + Future _pollWalletState( + SocialChatUserProfileManager profileManager, + ) async { + for (int index = 0; index < 2; index++) { + await Future.delayed(const Duration(seconds: 3)); + await _refreshWalletState(profileManager); + } + } + + Future _waitForRechargeResult( + SocialChatUserProfileManager profileManager, + double previousBalance, + ) async { + await _refreshWalletState(profileManager); + if (profileManager.myBalance > previousBalance) { + return true; + } + + for (int index = 0; index < 5; index++) { + await Future.delayed(const Duration(seconds: 2)); + await _refreshWalletState(profileManager); + if (profileManager.myBalance > previousBalance) { + return true; + } + } + + unawaited(_pollWalletState(profileManager)); + return false; + } + + SCMiFaPayCountryRes _fallbackCountry() { + return SCMiFaPayCountryRes( + id: defaultPayCountryId, + countryName: defaultPayCountryName, + alphaTwo: defaultPayCountryCode, + nationalFlag: '', + ); + } + + String _readErrorMessage(Object error) { + final String raw = error.toString(); + return raw + .replaceFirst('Exception: ', '') + .replaceFirst('DioException [unknown]: ', '') + .trim(); + } + + void _logRequest({ + required String method, + required String path, + Map? queryParams, + Map? body, + }) { + debugPrint('[MiFaPay][Request] $method ${_buildUrl(path)}'); + if (queryParams != null && queryParams.isNotEmpty) { + debugPrint('[MiFaPay][Query] ${_safeJson(queryParams)}'); + } + if (body != null && body.isNotEmpty) { + debugPrint('[MiFaPay][Body] ${_safeJson(body)}'); + } + } + + void _logResponse({required String tag, required dynamic data}) { + debugPrint('[MiFaPay][Response][$tag] ${_safeJson(data)}'); + } + + void _logError({ + required String tag, + required String path, + required Object error, + Map? queryParams, + Map? body, + }) { + debugPrint('[MiFaPay][Error][$tag] ${_buildUrl(path)}'); + if (queryParams != null && queryParams.isNotEmpty) { + debugPrint('[MiFaPay][ErrorQuery][$tag] ${_safeJson(queryParams)}'); + } + if (body != null && body.isNotEmpty) { + debugPrint('[MiFaPay][ErrorBody][$tag] ${_safeJson(body)}'); + } + + if (error is DioException) { + debugPrint('[MiFaPay][Dio][$tag] type=${error.type}'); + debugPrint( + '[MiFaPay][Dio][$tag] status=${error.response?.statusCode} message=${error.message}', + ); + if (error.requestOptions.path.isNotEmpty) { + debugPrint( + '[MiFaPay][Dio][$tag] requestPath=${error.requestOptions.path}', + ); + } + if (error.requestOptions.queryParameters.isNotEmpty) { + debugPrint( + '[MiFaPay][Dio][$tag] requestQuery=${_safeJson(error.requestOptions.queryParameters)}', + ); + } + if (error.requestOptions.data != null) { + debugPrint( + '[MiFaPay][Dio][$tag] requestData=${_safeJson(error.requestOptions.data)}', + ); + } + if (error.response?.data != null) { + debugPrint( + '[MiFaPay][Dio][$tag] responseData=${_safeJson(error.response?.data)}', + ); + } + } else { + debugPrint('[MiFaPay][Error][$tag] $error'); + } + } + + String _buildUrl(String path) { + final String host = SCGlobalConfig.apiHost; + if (host.endsWith('/') && path.startsWith('/')) { + return '${host.substring(0, host.length - 1)}$path'; + } + if (!host.endsWith('/') && !path.startsWith('/')) { + return '$host/$path'; + } + return '$host$path'; + } + + String _safeJson(dynamic data) { + try { + return jsonEncode(data); + } catch (_) { + return data.toString(); + } + } +} diff --git a/lib/shared/business_logic/models/res/follow_room_res.dart b/lib/shared/business_logic/models/res/follow_room_res.dart index 290c246..de2321b 100644 --- a/lib/shared/business_logic/models/res/follow_room_res.dart +++ b/lib/shared/business_logic/models/res/follow_room_res.dart @@ -53,8 +53,8 @@ class FollowRoomRes { /// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"s","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} /// userSVipLevel : "" -class RoomProfile { - RoomProfile({ +class RoomProfile { + RoomProfile({ String? countryCode, String? countryName, String? event, @@ -69,11 +69,12 @@ class RoomProfile { String? roomGameIcon, String? roomName, String? sysOrigin, - String? userId, - SocialChatUserProfile? userProfile, - String? userSVipLevel, - ExtValues? extValues, - }) { + String? userId, + SocialChatUserProfile? userProfile, + String? userSVipLevel, + RoomMemberCounter? roomCounter, + ExtValues? extValues, + }) { _countryCode = countryCode; _countryName = countryName; _event = event; @@ -88,11 +89,12 @@ class RoomProfile { _roomGameIcon = roomGameIcon; _roomName = roomName; _sysOrigin = sysOrigin; - _userId = userId; - _userProfile = userProfile; - _userSVipLevel = userSVipLevel; - _extValues = extValues; - } + _userId = userId; + _userProfile = userProfile; + _userSVipLevel = userSVipLevel; + _roomCounter = roomCounter; + _extValues = extValues; + } RoomProfile.fromJson(dynamic json) { _countryCode = json['countryCode']; @@ -113,15 +115,18 @@ class RoomProfile { _roomName = json['roomName']; _sysOrigin = json['sysOrigin']; _userId = json['userId']; - _userProfile = - json['userProfile'] != null - ? SocialChatUserProfile.fromJson(json['userProfile']) - : null; - _userSVipLevel = json['userSVipLevel']; - _extValues = - json['extValues'] != null - ? ExtValues.fromJson(json['extValues']) - : null; + _userProfile = + json['userProfile'] != null + ? SocialChatUserProfile.fromJson(json['userProfile']) + : null; + _userSVipLevel = json['userSVipLevel']; + final counterJson = json['roomCounter'] ?? json['counter']; + _roomCounter = + counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null; + _extValues = + json['extValues'] != null + ? ExtValues.fromJson(json['extValues']) + : null; } String? _countryCode; String? _countryName; @@ -137,10 +142,11 @@ class RoomProfile { String? _roomGameIcon; String? _roomName; String? _sysOrigin; - String? _userId; - SocialChatUserProfile? _userProfile; - String? _userSVipLevel; - ExtValues? _extValues; + String? _userId; + SocialChatUserProfile? _userProfile; + String? _userSVipLevel; + RoomMemberCounter? _roomCounter; + ExtValues? _extValues; RoomProfile copyWith({ String? countryCode, String? countryName, @@ -156,11 +162,12 @@ class RoomProfile { String? roomGameIcon, String? roomName, String? sysOrigin, - String? userId, - SocialChatUserProfile? userProfile, - String? userSVipLevel, - ExtValues? extValues, - }) => RoomProfile( + String? userId, + SocialChatUserProfile? userProfile, + String? userSVipLevel, + RoomMemberCounter? roomCounter, + ExtValues? extValues, + }) => RoomProfile( countryCode: countryCode ?? _countryCode, countryName: countryName ?? _countryName, event: event ?? _event, @@ -175,11 +182,12 @@ class RoomProfile { roomGameIcon: roomGameIcon ?? _roomGameIcon, roomName: roomName ?? _roomName, sysOrigin: sysOrigin ?? _sysOrigin, - userId: userId ?? _userId, - userProfile: userProfile ?? _userProfile, - userSVipLevel: userSVipLevel ?? _userSVipLevel, - extValues: extValues ?? _extValues, - ); + userId: userId ?? _userId, + userProfile: userProfile ?? _userProfile, + userSVipLevel: userSVipLevel ?? _userSVipLevel, + roomCounter: roomCounter ?? _roomCounter, + extValues: extValues ?? _extValues, + ); String? get countryCode => _countryCode; String? get countryName => _countryName; String? get event => _event; @@ -195,9 +203,25 @@ class RoomProfile { String? get roomName => _roomName; String? get sysOrigin => _sysOrigin; String? get userId => _userId; - SocialChatUserProfile? get userProfile => _userProfile; - String? get userSVipLevel => _userSVipLevel; - ExtValues? get extValues => _extValues; + SocialChatUserProfile? get userProfile => _userProfile; + String? get userSVipLevel => _userSVipLevel; + RoomMemberCounter? get roomCounter => _roomCounter; + ExtValues? get extValues => _extValues; + + String get displayMemberCount { + final memberCount = roomCounter?.memberCount; + if (memberCount != null) { + if (memberCount == memberCount.roundToDouble()) { + return memberCount.toInt().toString(); + } + return memberCount.toString(); + } + final memberQuantity = extValues?.memberQuantity; + if ((memberQuantity ?? "").trim().isNotEmpty) { + return memberQuantity!; + } + return "0"; + } Map toJson() { final map = {}; @@ -216,13 +240,16 @@ class RoomProfile { map['roomName'] = _roomName; map['sysOrigin'] = _sysOrigin; map['userId'] = _userId; - if (_userProfile != null) { - map['userProfile'] = _userProfile?.toJson(); - } - map['userSVipLevel'] = _userSVipLevel; - if (_extValues != null) { - map['extValues'] = _extValues?.toJson(); - } + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + map['userSVipLevel'] = _userSVipLevel; + if (_roomCounter != null) { + map['roomCounter'] = _roomCounter?.toJson(); + } + if (_extValues != null) { + map['extValues'] = _extValues?.toJson(); + } return map; } } diff --git a/lib/shared/business_logic/models/res/room_res.dart b/lib/shared/business_logic/models/res/room_res.dart index c1494a1..cae3e23 100644 --- a/lib/shared/business_logic/models/res/room_res.dart +++ b/lib/shared/business_logic/models/res/room_res.dart @@ -18,8 +18,8 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart'; /// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} /// userSVipLevel : "" -class SocialChatRoomRes { - SocialChatRoomRes({ +class SocialChatRoomRes { + SocialChatRoomRes({ String? countryCode, String? countryName, String? event, @@ -34,11 +34,12 @@ class SocialChatRoomRes { String? roomGameIcon, String? roomName, String? sysOrigin, - String? userId, - SocialChatUserProfile? userProfile, - String? userSVipLevel, - ExtValues? extValues, - }) { + String? userId, + SocialChatUserProfile? userProfile, + String? userSVipLevel, + RoomMemberCounter? roomCounter, + ExtValues? extValues, + }) { _countryCode = countryCode; _countryName = countryName; _event = event; @@ -53,11 +54,12 @@ class SocialChatRoomRes { _roomGameIcon = roomGameIcon; _roomName = roomName; _sysOrigin = sysOrigin; - _userId = userId; - _userProfile = userProfile; - _userSVipLevel = userSVipLevel; - _extValues = extValues; - } + _userId = userId; + _userProfile = userProfile; + _userSVipLevel = userSVipLevel; + _roomCounter = roomCounter; + _extValues = extValues; + } SocialChatRoomRes.fromJson(dynamic json) { _countryCode = json['countryCode']; @@ -80,15 +82,18 @@ class SocialChatRoomRes { _roomName = json['roomName']; _sysOrigin = json['sysOrigin']; _userId = json['userId']; - _userProfile = - json['userProfile'] != null - ? SocialChatUserProfile.fromJson(json['userProfile']) - : null; - _userSVipLevel = json['userSVipLevel']; - _extValues = - json['extValues'] != null - ? ExtValues.fromJson(json['extValues']) - : null; + _userProfile = + json['userProfile'] != null + ? SocialChatUserProfile.fromJson(json['userProfile']) + : null; + _userSVipLevel = json['userSVipLevel']; + final counterJson = json['roomCounter'] ?? json['counter']; + _roomCounter = + counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null; + _extValues = + json['extValues'] != null + ? ExtValues.fromJson(json['extValues']) + : null; } String? _countryCode; @@ -105,10 +110,11 @@ class SocialChatRoomRes { String? _roomGameIcon; String? _roomName; String? _sysOrigin; - String? _userId; - SocialChatUserProfile? _userProfile; - String? _userSVipLevel; - ExtValues? _extValues; + String? _userId; + SocialChatUserProfile? _userProfile; + String? _userSVipLevel; + RoomMemberCounter? _roomCounter; + ExtValues? _extValues; SocialChatRoomRes copyWith({ String? countryCode, @@ -125,11 +131,12 @@ class SocialChatRoomRes { String? roomGameIcon, String? roomName, String? sysOrigin, - String? userId, - SocialChatUserProfile? userProfile, - String? userSVipLevel, - ExtValues? extValues, - }) => SocialChatRoomRes( + String? userId, + SocialChatUserProfile? userProfile, + String? userSVipLevel, + RoomMemberCounter? roomCounter, + ExtValues? extValues, + }) => SocialChatRoomRes( countryCode: countryCode ?? _countryCode, countryName: countryName ?? _countryName, event: event ?? _event, @@ -144,11 +151,12 @@ class SocialChatRoomRes { roomGameIcon: roomGameIcon ?? _roomGameIcon, roomName: roomName ?? _roomName, sysOrigin: sysOrigin ?? _sysOrigin, - userId: userId ?? _userId, - userProfile: userProfile ?? _userProfile, - userSVipLevel: userSVipLevel ?? _userSVipLevel, - extValues: extValues ?? _extValues, - ); + userId: userId ?? _userId, + userProfile: userProfile ?? _userProfile, + userSVipLevel: userSVipLevel ?? _userSVipLevel, + roomCounter: roomCounter ?? _roomCounter, + extValues: extValues ?? _extValues, + ); String? get countryCode => _countryCode; @@ -180,11 +188,25 @@ class SocialChatRoomRes { String? get userId => _userId; - SocialChatUserProfile? get userProfile => _userProfile; - - String? get userSVipLevel => _userSVipLevel; - - ExtValues? get extValues => _extValues; + SocialChatUserProfile? get userProfile => _userProfile; + + String? get userSVipLevel => _userSVipLevel; + + RoomMemberCounter? get roomCounter => _roomCounter; + + ExtValues? get extValues => _extValues; + + String get displayMemberCount { + final memberCount = roomCounter?.memberCount; + if (memberCount != null) { + return _formatRoomMemberCount(memberCount); + } + final memberQuantity = extValues?.memberQuantity; + if ((memberQuantity ?? "").trim().isNotEmpty) { + return memberQuantity!; + } + return "0"; + } Map toJson() { final map = {}; @@ -203,16 +225,58 @@ class SocialChatRoomRes { map['roomName'] = _roomName; map['sysOrigin'] = _sysOrigin; map['userId'] = _userId; - if (_userProfile != null) { - map['userProfile'] = _userProfile?.toJson(); - } - map['userSVipLevel'] = _userSVipLevel; - if (_extValues != null) { - map['extValues'] = _extValues?.toJson(); - } - return map; - } -} + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + map['userSVipLevel'] = _userSVipLevel; + if (_roomCounter != null) { + map['roomCounter'] = _roomCounter?.toJson(); + } + if (_extValues != null) { + map['extValues'] = _extValues?.toJson(); + } + return map; + } +} + +String _formatRoomMemberCount(num value) { + if (value == value.roundToDouble()) { + return value.toInt().toString(); + } + return value.toString(); +} + +class RoomMemberCounter { + RoomMemberCounter({num? adminCount, num? memberCount}) { + _adminCount = adminCount; + _memberCount = memberCount; + } + + RoomMemberCounter.fromJson(dynamic json) { + _adminCount = json['adminCount']; + _memberCount = json['memberCount']; + } + + num? _adminCount; + num? _memberCount; + + RoomMemberCounter copyWith({num? adminCount, num? memberCount}) => + RoomMemberCounter( + adminCount: adminCount ?? _adminCount, + memberCount: memberCount ?? _memberCount, + ); + + num? get adminCount => _adminCount; + + num? get memberCount => _memberCount; + + Map toJson() { + final map = {}; + map['adminCount'] = _adminCount; + map['memberCount'] = _memberCount; + return map; + } +} /// account : "" /// accountStatus : "" diff --git a/lib/shared/business_logic/models/res/sc_mifa_pay_res.dart b/lib/shared/business_logic/models/res/sc_mifa_pay_res.dart new file mode 100644 index 0000000..c4db1f6 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_mifa_pay_res.dart @@ -0,0 +1,317 @@ +class SCMiFaPayCountryRes { + SCMiFaPayCountryRes({ + String? id, + String? countryName, + String? alphaTwo, + String? nationalFlag, + }) { + _id = id; + _countryName = countryName; + _alphaTwo = alphaTwo; + _nationalFlag = nationalFlag; + } + + SCMiFaPayCountryRes.fromJson(dynamic json) { + final dynamic country = json['country']; + _id = _stringValue(json['id']); + _countryName = _stringValue(json['countryName']); + _alphaTwo = _stringValue(country is Map ? country['alphaTwo'] : null); + _nationalFlag = _stringValue( + country is Map ? country['nationalFlag'] : null, + ); + } + + String? _id; + String? _countryName; + String? _alphaTwo; + String? _nationalFlag; + + String? get id => _id; + + String? get countryName => _countryName; + + String? get alphaTwo => _alphaTwo; + + String? get nationalFlag => _nationalFlag; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['countryName'] = _countryName; + map['alphaTwo'] = _alphaTwo; + map['nationalFlag'] = _nationalFlag; + return map; + } +} + +class SCMiFaPayApplicationRes { + SCMiFaPayApplicationRes({String? id, String? appName, String? appCode}) { + _id = id; + _appName = appName; + _appCode = appCode; + } + + SCMiFaPayApplicationRes.fromJson(dynamic json) { + _id = _stringValue(json['id']); + _appName = _stringValue(json['appName']); + _appCode = _stringValue(json['appCode']); + } + + String? _id; + String? _appName; + String? _appCode; + + String? get id => _id; + + String? get appName => _appName; + + String? get appCode => _appCode; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['appName'] = _appName; + map['appCode'] = _appCode; + return map; + } +} + +class SCMiFaPayCommodityItemRes { + SCMiFaPayCommodityItemRes({ + String? id, + String? content, + String? awardContent, + num? amountUsd, + num? amount, + String? currency, + }) { + _id = id; + _content = content; + _awardContent = awardContent; + _amountUsd = amountUsd; + _amount = amount; + _currency = currency; + } + + SCMiFaPayCommodityItemRes.fromJson(dynamic json) { + _id = _stringValue(json['id']); + _content = _stringValue(json['content']); + _awardContent = _stringValue(json['awardContent']); + _amountUsd = _numValue(json['amountUsd']); + _amount = _numValue(json['amount']); + _currency = _stringValue(json['currency']); + } + + String? _id; + String? _content; + String? _awardContent; + num? _amountUsd; + num? _amount; + String? _currency; + + String? get id => _id; + + String? get content => _content; + + String? get awardContent => _awardContent; + + num? get amountUsd => _amountUsd; + + num? get amount => _amount; + + String? get currency => _currency; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['content'] = _content; + map['awardContent'] = _awardContent; + map['amountUsd'] = _amountUsd; + map['amount'] = _amount; + map['currency'] = _currency; + return map; + } +} + +class SCMiFaPayChannelRes { + SCMiFaPayChannelRes({ + String? channelCode, + String? channelName, + String? channelType, + String? factoryChannel, + num? suggestScore, + }) { + _channelCode = channelCode; + _channelName = channelName; + _channelType = channelType; + _factoryChannel = factoryChannel; + _suggestScore = suggestScore; + } + + SCMiFaPayChannelRes.fromJson(dynamic json) { + final dynamic channel = json['channel']; + final dynamic details = json['details']; + _channelCode = _stringValue(channel is Map ? channel['channelCode'] : null); + _channelName = _stringValue(channel is Map ? channel['channelName'] : null); + _channelType = _stringValue(channel is Map ? channel['channelType'] : null); + _factoryChannel = _stringValue( + details is Map ? details['factoryChannel'] : null, + ); + _suggestScore = _numValue(details is Map ? details['suggestScore'] : null); + } + + String? _channelCode; + String? _channelName; + String? _channelType; + String? _factoryChannel; + num? _suggestScore; + + String? get channelCode => _channelCode; + + String? get channelName => _channelName; + + String? get channelType => _channelType; + + String? get factoryChannel => _factoryChannel; + + num? get suggestScore => _suggestScore; + + Map toJson() { + final map = {}; + map['channelCode'] = _channelCode; + map['channelName'] = _channelName; + map['channelType'] = _channelType; + map['factoryChannel'] = _factoryChannel; + map['suggestScore'] = _suggestScore; + return map; + } +} + +class SCMiFaPayCommodityRes { + SCMiFaPayCommodityRes({ + SCMiFaPayApplicationRes? application, + List? commodity, + List? channels, + }) { + _application = application; + _commodity = commodity; + _channels = channels; + } + + SCMiFaPayCommodityRes.fromJson(dynamic json) { + _application = + json['application'] != null + ? SCMiFaPayApplicationRes.fromJson(json['application']) + : null; + _commodity = + (json['commodity'] as List?) + ?.map((dynamic item) => SCMiFaPayCommodityItemRes.fromJson(item)) + .toList() ?? + []; + _channels = + (json['channels'] as List?) + ?.map((dynamic item) => SCMiFaPayChannelRes.fromJson(item)) + .toList() ?? + []; + } + + SCMiFaPayApplicationRes? _application; + List? _commodity; + List? _channels; + + SCMiFaPayApplicationRes? get application => _application; + + List? get commodity => _commodity; + + List? get channels => _channels; + + Map toJson() { + final map = {}; + map['application'] = _application?.toJson(); + map['commodity'] = _commodity?.map((item) => item.toJson()).toList(); + map['channels'] = _channels?.map((item) => item.toJson()).toList(); + return map; + } +} + +class SCMiFaPayRechargeRes { + SCMiFaPayRechargeRes({ + String? tradeNo, + String? orderId, + String? factoryCode, + String? factoryChannelCode, + String? currency, + String? countryCode, + String? requestUrl, + }) { + _tradeNo = tradeNo; + _orderId = orderId; + _factoryCode = factoryCode; + _factoryChannelCode = factoryChannelCode; + _currency = currency; + _countryCode = countryCode; + _requestUrl = requestUrl; + } + + SCMiFaPayRechargeRes.fromJson(dynamic json) { + _tradeNo = _stringValue(json['tradeNo']); + _orderId = _stringValue(json['orderId']); + _factoryCode = _stringValue(json['factoryCode']); + _factoryChannelCode = _stringValue(json['factoryChannelCode']); + _currency = _stringValue(json['currency']); + _countryCode = _stringValue(json['countryCode']); + _requestUrl = _stringValue(json['requestUrl']); + } + + String? _tradeNo; + String? _orderId; + String? _factoryCode; + String? _factoryChannelCode; + String? _currency; + String? _countryCode; + String? _requestUrl; + + String? get tradeNo => _tradeNo; + + String? get orderId => _orderId; + + String? get factoryCode => _factoryCode; + + String? get factoryChannelCode => _factoryChannelCode; + + String? get currency => _currency; + + String? get countryCode => _countryCode; + + String? get requestUrl => _requestUrl; + + Map toJson() { + final map = {}; + map['tradeNo'] = _tradeNo; + map['orderId'] = _orderId; + map['factoryCode'] = _factoryCode; + map['factoryChannelCode'] = _factoryChannelCode; + map['currency'] = _currency; + map['countryCode'] = _countryCode; + map['requestUrl'] = _requestUrl; + return map; + } +} + +String? _stringValue(dynamic value) { + if (value == null) { + return null; + } + final String parsed = value.toString(); + return parsed.isEmpty ? null : parsed; +} + +num? _numValue(dynamic value) { + if (value == null) { + return null; + } + if (value is num) { + return value; + } + return num.tryParse(value.toString()); +} diff --git a/lib/shared/business_logic/repositories/config_repository.dart b/lib/shared/business_logic/repositories/config_repository.dart index baa9262..e5aa227 100644 --- a/lib/shared/business_logic/repositories/config_repository.dart +++ b/lib/shared/business_logic/repositories/config_repository.dart @@ -1,6 +1,7 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/country_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_level_config_res.dart'; @@ -26,15 +27,17 @@ abstract class SocialChatConfigRepository { ///最近活动通知列表. Future getNoticeMessage(); + ///最近活动排行 + Future> topFourWithReward(); - - + ///获得SudCode + Future getSudCode(); ///获得客服. Future getCustomerService(); ///获取平台banner. - Future> getBanner({List?types}); + Future> getBanner({List? types}); ///获取商品配置 Future> productConfig(); @@ -55,11 +58,28 @@ abstract class SocialChatConfigRepository { String? friendId, }); + ///MiFaPay 可支付国家 + Future> mifaPayCountries(); + + ///MiFaPay 商品与渠道 + Future mifaPayCommodity({ + required String applicationId, + required String payCountryId, + String type = 'GOLD', + }); + + ///MiFaPay 下单 + Future mifaPayRecharge({ + required String applicationId, + required String goodsId, + required String payCountryId, + required String userId, + required String channelCode, + }); + ///等级资源 Future configLevel(); - - ///获取APP最新版本 Future versionManageLatest(); @@ -68,5 +88,4 @@ abstract class SocialChatConfigRepository { ///获得客服 Future customerService(); - } diff --git a/lib/shared/data_sources/models/message/sc_floating_message.dart b/lib/shared/data_sources/models/message/sc_floating_message.dart index 63f9e4f..d964c27 100644 --- a/lib/shared/data_sources/models/message/sc_floating_message.dart +++ b/lib/shared/data_sources/models/message/sc_floating_message.dart @@ -1,69 +1,73 @@ -class SCFloatingMessage { - String? userId; // 可选:用户ID - String? roomId; // 可选:房间ID - String? toUserId; // 可选:用户ID - String? userAvatarUrl; // 用户头像 - String? toUserAvatarUrl; // 用户头像 - String? userName; // 用户昵称 - String? toUserName; // 用户昵称 - String? giftUrl; // 礼物图标 - int? type; - int? rocketLevel; - num? coins; - num? number; - num? multiple; - int priority = 10; //排序权重 - - SCFloatingMessage({ - this.type = 0, - this.rocketLevel = 0, - this.userId = '', - this.roomId = '', - this.toUserId = '', - this.userAvatarUrl = '', - this.toUserAvatarUrl = '', - this.userName = '', - this.toUserName = '', - this.giftUrl = '', - this.number = 0, - this.coins = 0, - this.priority = 10, - this.multiple = 10, - }); - - SCFloatingMessage.fromJson(dynamic json) { - userId = json['userId']; - rocketLevel = json['rocketLevel']; - toUserId = json['toUserId']; - roomId = json['roomId']; - type = json['type']; - userAvatarUrl = json['userAvatarUrl']; - toUserAvatarUrl = json['toUserAvatarUrl']; - userName = json['userName']; - toUserName = json['toUserName']; - giftUrl = json['giftUrl']; - coins = json['coins']; - number = json['number']; - priority = json['priority']; - multiple = json['multiple']; - } - - Map toJson() { - final map = {}; - map['userId'] = userId; - map['rocketLevel'] = rocketLevel; - map['roomId'] = roomId; - map['type'] = type; - map['toUserId'] = toUserId; - map['userAvatarUrl'] = userAvatarUrl; - map['toUserAvatarUrl'] = toUserAvatarUrl; - map['userName'] = userName; - map['toUserName'] = toUserName; - map['giftUrl'] = giftUrl; - map['coins'] = coins; - map['number'] = number; - map['priority'] = priority; - map['multiple'] = multiple; - return map; - } -} +class SCFloatingMessage { + String? userId; // 可选:用户ID + String? roomId; // 可选:房间ID + String? toUserId; // 可选:用户ID + String? userAvatarUrl; // 用户头像 + String? toUserAvatarUrl; // 用户头像 + String? userName; // 用户昵称 + String? toUserName; // 用户昵称 + String? giftUrl; // 礼物图标 + int? type; + int? rocketLevel; + num? coins; + num? number; + num? multiple; + int priority = 10; //排序权重 + int aggregateVersion = 0; + + SCFloatingMessage({ + this.type = 0, + this.rocketLevel = 0, + this.userId = '', + this.roomId = '', + this.toUserId = '', + this.userAvatarUrl = '', + this.toUserAvatarUrl = '', + this.userName = '', + this.toUserName = '', + this.giftUrl = '', + this.number = 0, + this.coins = 0, + this.priority = 10, + this.multiple = 10, + this.aggregateVersion = 0, + }); + + SCFloatingMessage.fromJson(dynamic json) { + userId = json['userId']; + rocketLevel = json['rocketLevel']; + toUserId = json['toUserId']; + roomId = json['roomId']; + type = json['type']; + userAvatarUrl = json['userAvatarUrl']; + toUserAvatarUrl = json['toUserAvatarUrl']; + userName = json['userName']; + toUserName = json['toUserName']; + giftUrl = json['giftUrl']; + coins = json['coins']; + number = json['number']; + priority = json['priority']; + multiple = json['multiple']; + aggregateVersion = json['aggregateVersion'] ?? 0; + } + + Map toJson() { + final map = {}; + map['userId'] = userId; + map['rocketLevel'] = rocketLevel; + map['roomId'] = roomId; + map['type'] = type; + map['toUserId'] = toUserId; + map['userAvatarUrl'] = userAvatarUrl; + map['toUserAvatarUrl'] = toUserAvatarUrl; + map['userName'] = userName; + map['toUserName'] = toUserName; + map['giftUrl'] = giftUrl; + map['coins'] = coins; + map['number'] = number; + map['priority'] = priority; + map['multiple'] = multiple; + map['aggregateVersion'] = aggregateVersion; + return map; + } +} diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart index b1adbf2..59f4b62 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -20,12 +20,12 @@ class OverlayManager { ); bool _isPlaying = false; OverlayEntry? _currentOverlayEntry; + SCFloatingMessage? _currentMessage; bool _isProcessing = false; bool _isDisposed = false; - static final OverlayManager _instance = - OverlayManager._internal(); + static final OverlayManager _instance = OverlayManager._internal(); factory OverlayManager() => _instance; @@ -34,16 +34,87 @@ class OverlayManager { void addMessage(SCFloatingMessage message) { if (_isDisposed) return; if (SCGlobalConfig.isFloatingAnimationInGlobal) { + if (_tryAggregateMessage(message)) { + return; + } _messageQueue.add(message); _safeScheduleNext(); } else { _messageQueue.clear(); - _isPlaying = false; + _isPlaying = false; _isProcessing = false; - _isDisposed = false; + _isDisposed = false; } } + bool _tryAggregateMessage(SCFloatingMessage incoming) { + if (!_supportsAggregation(incoming)) { + return false; + } + if (_currentMessage != null && + _isSameAggregatedFloatingMessage(_currentMessage!, incoming)) { + _mergeFloatingMessage(_currentMessage!, incoming); + _currentOverlayEntry?.markNeedsBuild(); + return true; + } + for (final queuedMessage in _messageQueue.unorderedElements) { + if (_isSameAggregatedFloatingMessage(queuedMessage, incoming)) { + _mergeFloatingMessage(queuedMessage, incoming); + return true; + } + } + return false; + } + + bool _supportsAggregation(SCFloatingMessage message) { + return message.type == 0; + } + + bool _isSameAggregatedFloatingMessage( + SCFloatingMessage existing, + SCFloatingMessage incoming, + ) { + return existing.type == incoming.type && + existing.roomId == incoming.roomId && + existing.userId == incoming.userId && + existing.toUserId == incoming.toUserId && + existing.giftUrl == incoming.giftUrl; + } + + void _mergeFloatingMessage( + SCFloatingMessage target, + SCFloatingMessage incoming, + ) { + target.coins = (target.coins ?? 0) + (incoming.coins ?? 0); + target.number = (target.number ?? 0) + (incoming.number ?? 0); + final currentMultiple = target.multiple ?? 0; + final incomingMultiple = incoming.multiple ?? 0; + target.multiple = + currentMultiple >= incomingMultiple + ? currentMultiple + : incomingMultiple; + if ((target.userAvatarUrl ?? '').isEmpty) { + target.userAvatarUrl = incoming.userAvatarUrl; + } + if ((target.userName ?? '').isEmpty) { + target.userName = incoming.userName; + } + if ((target.toUserName ?? '').isEmpty) { + target.toUserName = incoming.toUserName; + } + if ((target.giftUrl ?? '').isEmpty) { + target.giftUrl = incoming.giftUrl; + } + if ((target.roomId ?? '').isEmpty) { + target.roomId = incoming.roomId; + } + target.priority = + target.priority >= incoming.priority + ? target.priority + : incoming.priority; + target.aggregateVersion += 1; + } + void _safeScheduleNext() { if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; _isProcessing = true; @@ -67,7 +138,10 @@ class OverlayManager { final messageToProcess = _messageQueue.first; if (messageToProcess?.type == 1) { - final rtcProvider = Provider.of(context, listen: false); + final rtcProvider = Provider.of( + context, + listen: false, + ); if (rtcProvider.currenRoom == null) { // 从队列中移除第一个元素 _messageQueue.removeFirst(); @@ -88,9 +162,11 @@ class OverlayManager { void _playMessage(SCFloatingMessage message) { _isPlaying = true; + _currentMessage = message; final context = navigatorKey.currentState?.context; if (context == null || !context.mounted) { _isPlaying = false; + _currentMessage = null; _safeScheduleNext(); return; } @@ -106,7 +182,7 @@ class OverlayManager { ), ); - Overlay.of(context)?.insert(_currentOverlayEntry!); + Overlay.of(context).insert(_currentOverlayEntry!); } Widget _buildScreenWidget(SCFloatingMessage message) { @@ -119,10 +195,12 @@ class OverlayManager { try { _currentOverlayEntry?.remove(); _currentOverlayEntry = null; + _currentMessage = null; _isPlaying = false; _safeScheduleNext(); } catch (e) { debugPrint('清理悬浮消息出错: $e'); + _currentMessage = null; _isPlaying = false; _safeScheduleNext(); } @@ -131,6 +209,9 @@ class OverlayManager { switch (message.type) { case 0: return FloatingLuckGiftScreenWidget( + key: ValueKey( + 'luck_${message.userId}_${message.toUserId}_${message.giftUrl}_${message.aggregateVersion}', + ), message: message, onAnimationCompleted: onComplete, ); @@ -172,6 +253,7 @@ class OverlayManager { _isDisposed = true; _currentOverlayEntry?.remove(); _currentOverlayEntry = null; + _currentMessage = null; _messageQueue.clear(); _isPlaying = false; _isProcessing = false; diff --git a/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart index 85b7613..a09de20 100644 --- a/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart @@ -1,229 +1,284 @@ -import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_version_manage_latest_res.dart'; -import 'package:yumi/shared/business_logic/models/res/country_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_level_config_res.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart'; -import 'package:yumi/shared/business_logic/models/res/version_manage_lates_review_res.dart'; -import 'package:yumi/shared/business_logic/repositories/config_repository.dart'; -import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; - -class SCConfigRepositoryImp implements SocialChatConfigRepository { - static SCConfigRepositoryImp? _instance; - - SCConfigRepositoryImp._internal(); - - factory SCConfigRepositoryImp() { - return _instance ??= SCConfigRepositoryImp._internal(); - } - - ///sys/config/country - @override - Future loadCountry() async { - final result = await http.get( - "9c370c6fef50fc728152e7a3fd5b47d2841a1d9a5a2b0bb769039fdd47f01585", - fromJson: (json) => CountryRes.fromJson(json), - ); - return result; - } - - ///sys/config/banner - @override - Future> getBanner({List?types}) async { - Map params = {}; - if (types != null) { - params["types"] = types; - } - final result = await http.get( - "0d8319c7a696c73d58f5e0ae304dc663f574ade4154ed90ccaee524f6ba14490", - queryParams: params, - fromJson: - (json) => - (json as List).map((e) => SCIndexBannerRes.fromJson(e)).toList(), - ); - return result; - } - - ///sys/config/enum/config - @override - Future getConfig() async { - final result = await http.get( - "e6fcae3c4a41806e15b4fc015126361267972b2854bbea42d0f0089c5f1c7724", - fromJson: (json) => CountryRes.fromJson(json), - ); - return result; - } - - ///sys/config/customer-service - @override - Future getCustomerService() async { - final result = await http.get( - "ba316258c14cc3ebddb6d28ec314bc5704e593861ca693058e9e98ab3114cf05", - fromJson: (json) => CountryRes.fromJson(json), - ); - return result; - } - - - - ///ranking/top-four-with-reward - @override - Future> topFourWithReward() async { - final result = await http.post>( - "162c2587d0236c6122a197f872daf865c29cc45b9c80570e9f4432051b2040e4", - data: {}, - fromJson: - (json) => - (json as List) - .map((e) => SCTopFourWithRewardRes.fromJson(e)) - .toList(), - ); - return result; - } - - ///sys/config/sys/notice-message - @override - Future getNoticeMessage() async { - final result = await http.get( - "b45ab3738ec718525eb02dcc8b1c79165ae6295dd8ec0e521307c70a005119b2", - fromJson: (json) => CountryRes.fromJson(json), - ); - return result; - } - - - ///sys/config/banner/start-page - @override - Future> getStartPage() async { - final result = await http.get( - "0d8319c7a696c73d58f5e0ae304dc66352cfd4d4480504cd5f589338f4f90157", - fromJson: - (json) => - (json as List).map((e) => SCStartPageRes.fromJson(e)).toList(), - ); - return result; - } - - ///sys/config/room/getSudCode - @override - Future getSudCode() async { - final result = await http.get( - "690f390b1c91b6cb0b4fdbeb98215a9f2fb1663213697d032f7fe4dd9aa9e8f4", - fromJson: (json) => CountryRes.fromJson(json), - ); - return result; - } - - ///sys/config/country/top-six - @override - Future loadTopSix() async { - final result = await http.get( - "9c370c6fef50fc728152e7a3fd5b47d263ca03b0e63d90aeb3c57f8d946ecd78", - fromJson: (json) => CountryRes.fromJson(json), - ); - return result; - } - - ///order/product-config - @override - Future> productConfig() async { - final result = await http.get>( - "1585c72b88d0d249c7078aae852a0806cc3d8e483b8d861962a977a00523180a", - fromJson: - (json) => - (json as List).map((e) => SCProductConfigRes.fromJson(e)).toList(), - ); - return result; - } - - ///order/purchase-pay/google - @override - Future googlePay( - String product, - String signature, - String purchaseData, { - String? friendId, - }) async { - Map params = {}; - params["product"] = product; - params["signature"] = signature; - params["purchaseData"] = purchaseData; - if (friendId != null) { - params["friendId"] = friendId; - } - final result = await http.post( - "3a5ab504e90faccde8258f4ea1dd6bcdad6eec89ad8cc807b184537e0fdb6904", - data: params, - fromJson: (json) => SCGooglePayRes.fromJson(json), - ); - return result; - } - - ///order/purchase-pay/apple - @override - Future applePay( - String product, - String receipt, - String transaction, { - String? friendId, - }) async { - Map params = {}; - params["product"] = product; - params["receipt"] = receipt; - params["transaction"] = transaction; - if (friendId != null) { - params["friendId"] = friendId; - } - final result = await http.post( - "3a5ab504e90faccde8258f4ea1dd6bcd7e041553c5af750a6eff64c18e7de276", - data: params, - fromJson: (json) => SCGooglePayRes.fromJson(json), - ); - return result; - } - - ///sys/static-config/level - @override - Future configLevel() async { - final result = await http.get( - "9d964981940fe0403d53d6f44e7cf7a48c2534f1e31f6b5fa9b1fae672b836d1", - fromJson: (json) => SCLevelConfigRes.fromJson(json), - ); - return result; - } - - - ///sys/version/manage/release/latest - @override - Future versionManageLatest() async{ - final result = await http.get( - "ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687", - fromJson: (json) => SCVersionManageLatestRes.fromJson(json), - ); - return result; - } - - ///sys/version/manage/latest/review - @override - Future versionManageLatestReview() async{ - final result = await http.get( - "ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232", - fromJson: (json) => VersionManageLatesReviewRes.fromJson(json), - ); - return result; - } - - ///sys/config/customer-service - @override - Future customerService() async{ - final result = await http.get( - "ba316258c14cc3ebddb6d28ec314bc5704e593861ca693058e9e98ab3114cf05", - fromJson: (json) => SocialChatUserProfile.fromJson(json), - ); - return result; - } - -} +import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_version_manage_latest_res.dart'; +import 'package:yumi/shared/business_logic/models/res/country_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_level_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart'; +import 'package:yumi/shared/business_logic/models/res/version_manage_lates_review_res.dart'; +import 'package:yumi/shared/business_logic/repositories/config_repository.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; + +class SCConfigRepositoryImp implements SocialChatConfigRepository { + static SCConfigRepositoryImp? _instance; + + SCConfigRepositoryImp._internal(); + + factory SCConfigRepositoryImp() { + return _instance ??= SCConfigRepositoryImp._internal(); + } + + ///sys/config/country + @override + Future loadCountry() async { + final result = await http.get( + "9c370c6fef50fc728152e7a3fd5b47d2841a1d9a5a2b0bb769039fdd47f01585", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///sys/config/banner + @override + Future> getBanner({List? types}) async { + Map params = {}; + if (types != null) { + params["types"] = types; + } + final result = await http.get( + "0d8319c7a696c73d58f5e0ae304dc663f574ade4154ed90ccaee524f6ba14490", + queryParams: params, + fromJson: + (json) => + (json as List).map((e) => SCIndexBannerRes.fromJson(e)).toList(), + ); + return result; + } + + ///sys/config/enum/config + @override + Future getConfig() async { + final result = await http.get( + "e6fcae3c4a41806e15b4fc015126361267972b2854bbea42d0f0089c5f1c7724", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///sys/config/customer-service + @override + Future getCustomerService() async { + final result = await http.get( + "ba316258c14cc3ebddb6d28ec314bc5704e593861ca693058e9e98ab3114cf05", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///ranking/top-four-with-reward + @override + Future> topFourWithReward() async { + final result = await http.post>( + "162c2587d0236c6122a197f872daf865c29cc45b9c80570e9f4432051b2040e4", + data: {}, + fromJson: + (json) => + (json as List) + .map((e) => SCTopFourWithRewardRes.fromJson(e)) + .toList(), + ); + return result; + } + + ///sys/config/sys/notice-message + @override + Future getNoticeMessage() async { + final result = await http.get( + "b45ab3738ec718525eb02dcc8b1c79165ae6295dd8ec0e521307c70a005119b2", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///sys/config/banner/start-page + @override + Future> getStartPage() async { + final result = await http.get( + "0d8319c7a696c73d58f5e0ae304dc66352cfd4d4480504cd5f589338f4f90157", + fromJson: + (json) => + (json as List).map((e) => SCStartPageRes.fromJson(e)).toList(), + ); + return result; + } + + ///sys/config/room/getSudCode + @override + Future getSudCode() async { + final result = await http.get( + "690f390b1c91b6cb0b4fdbeb98215a9f2fb1663213697d032f7fe4dd9aa9e8f4", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///sys/config/country/top-six + @override + Future loadTopSix() async { + final result = await http.get( + "9c370c6fef50fc728152e7a3fd5b47d263ca03b0e63d90aeb3c57f8d946ecd78", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///order/product-config + @override + Future> productConfig() async { + final result = await http.get>( + "1585c72b88d0d249c7078aae852a0806cc3d8e483b8d861962a977a00523180a", + fromJson: + (json) => + (json as List) + .map((e) => SCProductConfigRes.fromJson(e)) + .toList(), + ); + return result; + } + + ///order/purchase-pay/google + @override + Future googlePay( + String product, + String signature, + String purchaseData, { + String? friendId, + }) async { + Map params = {}; + params["product"] = product; + params["signature"] = signature; + params["purchaseData"] = purchaseData; + if (friendId != null) { + params["friendId"] = friendId; + } + final result = await http.post( + "3a5ab504e90faccde8258f4ea1dd6bcdad6eec89ad8cc807b184537e0fdb6904", + data: params, + fromJson: (json) => SCGooglePayRes.fromJson(json), + ); + return result; + } + + ///order/purchase-pay/apple + @override + Future applePay( + String product, + String receipt, + String transaction, { + String? friendId, + }) async { + Map params = {}; + params["product"] = product; + params["receipt"] = receipt; + params["transaction"] = transaction; + if (friendId != null) { + params["friendId"] = friendId; + } + final result = await http.post( + "3a5ab504e90faccde8258f4ea1dd6bcd7e041553c5af750a6eff64c18e7de276", + data: params, + fromJson: (json) => SCGooglePayRes.fromJson(json), + ); + return result; + } + + ///order/web/pay/country + @override + Future> mifaPayCountries() async { + final result = await http.get>( + "/order/web/pay/country", + fromJson: + (json) => + (json as List) + .map((dynamic item) => SCMiFaPayCountryRes.fromJson(item)) + .toList(), + ); + return result; + } + + ///order/web/pay/commodity + @override + Future mifaPayCommodity({ + required String applicationId, + required String payCountryId, + String type = 'GOLD', + }) async { + final result = await http.get( + "/order/web/pay/commodity", + queryParams: { + "applicationId": applicationId, + "payCountryId": payCountryId, + "type": type, + }, + fromJson: (json) => SCMiFaPayCommodityRes.fromJson(json), + ); + return result; + } + + ///order/web/pay/recharge + @override + Future mifaPayRecharge({ + required String applicationId, + required String goodsId, + required String payCountryId, + required String userId, + required String channelCode, + }) async { + final result = await http.post( + "/order/web/pay/recharge", + data: { + "applicationId": applicationId, + "goodsId": goodsId, + "payCountryId": payCountryId, + "userId": userId, + "channelCode": channelCode, + "newVersion": true, + }, + fromJson: (json) => SCMiFaPayRechargeRes.fromJson(json), + ); + return result; + } + + ///sys/static-config/level + @override + Future configLevel() async { + final result = await http.get( + "9d964981940fe0403d53d6f44e7cf7a48c2534f1e31f6b5fa9b1fae672b836d1", + fromJson: (json) => SCLevelConfigRes.fromJson(json), + ); + return result; + } + + ///sys/version/manage/release/latest + @override + Future versionManageLatest() async { + final result = await http.get( + "ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687", + fromJson: (json) => SCVersionManageLatestRes.fromJson(json), + ); + return result; + } + + ///sys/version/manage/latest/review + @override + Future versionManageLatestReview() async { + final result = await http.get( + "ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232", + fromJson: (json) => VersionManageLatesReviewRes.fromJson(json), + ); + return result; + } + + ///sys/config/customer-service + @override + Future customerService() async { + final result = await http.get( + "ba316258c14cc3ebddb6d28ec314bc5704e593861ca693058e9e98ab3114cf05", + fromJson: (json) => SocialChatUserProfile.fromJson(json), + ); + return result; + } +} diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index ff61636..340487f 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -13,9 +13,13 @@ import 'package:tancent_vap/widgets/vap_view.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; class SCGiftVapSvgaManager { + static const int _maxPendingTaskCount = 18; Map videoItemCache = {}; static SCGiftVapSvgaManager? _inst; static const int _maxPreloadConcurrency = 1; + static const int _maxPreloadQueueLength = 12; + static const int _maxSvgaCacheEntries = 8; + static const int _maxPlayablePathCacheEntries = 24; SCGiftVapSvgaManager._internal(); @@ -86,7 +90,10 @@ class SCGiftVapSvgaManager { } Future preload(String path, {bool highPriority = false}) async { - if (path.isEmpty || _dis || _isPreloadedOrLoading(path)) { + if (!SCGlobalConfig.allowsHighCostAnimations || + path.isEmpty || + _dis || + _isPreloadedOrLoading(path)) { return; } if (highPriority) { @@ -97,12 +104,20 @@ class SCGiftVapSvgaManager { if (_queuedPreloadPaths.contains(path)) { return; } + _trimPreloadQueue(); _preloadQueue.add(path); _queuedPreloadPaths.add(path); _log('enqueue preload path=$path queue=${_preloadQueue.length}'); _drainPreloadQueue(); } + void _trimPreloadQueue() { + while (_preloadQueue.length >= _maxPreloadQueueLength) { + final removedPath = _preloadQueue.removeFirst(); + _queuedPreloadPaths.remove(removedPath); + } + } + void _drainPreloadQueue() { if (_dis || _pause || @@ -137,7 +152,7 @@ class SCGiftVapSvgaManager { } Future _loadSvgaEntity(String path) async { - final cached = videoItemCache[path]; + final cached = _touchCachedSvgaEntity(path); if (cached != null) { return cached; } @@ -159,7 +174,9 @@ class SCGiftVapSvgaManager { throw Exception('Unsupported SVGA path: $path'); } entity.autorelease = false; - videoItemCache[path] = entity; + if (!_dis) { + _cacheSvgaEntity(path, entity); + } return entity; }(); _svgaLoadTasks[path] = future; @@ -175,7 +192,7 @@ class SCGiftVapSvgaManager { if (pathType == PathType.asset || pathType == PathType.file) { return path; } - final cachedPath = _playablePathCache[path]; + final cachedPath = _touchCachedPlayablePath(path); if (cachedPath != null && cachedPath.isNotEmpty && File(cachedPath).existsSync()) { @@ -187,7 +204,9 @@ class SCGiftVapSvgaManager { } final future = () async { final file = await FileCacheManager.getInstance().getFile(url: path); - _playablePathCache[path] = file.path; + if (!_dis) { + _cachePlayablePath(path, file.path); + } return file.path; }(); _playablePathTasks[path] = future; @@ -198,6 +217,41 @@ class SCGiftVapSvgaManager { } } + MovieEntity? _touchCachedSvgaEntity(String path) { + final cached = videoItemCache.remove(path); + if (cached != null) { + videoItemCache[path] = cached; + } + return cached; + } + + void _cacheSvgaEntity(String path, MovieEntity entity) { + videoItemCache.remove(path); + videoItemCache[path] = entity; + _trimResolvedCache(videoItemCache, _maxSvgaCacheEntries); + } + + String? _touchCachedPlayablePath(String path) { + final cachedPath = _playablePathCache.remove(path); + if (cachedPath != null) { + _playablePathCache[path] = cachedPath; + } + return cachedPath; + } + + void _cachePlayablePath(String path, String playablePath) { + _playablePathCache.remove(path); + _playablePathCache[path] = playablePath; + _trimResolvedCache(_playablePathCache, _maxPlayablePathCacheEntries); + } + + void _trimResolvedCache(Map cache, int maxEntries) { + while (cache.length > maxEntries) { + final oldestKey = cache.keys.first; + cache.remove(oldestKey); + } + } + void _scheduleNextTask({Duration delay = Duration.zero}) { if (_dis) { return; @@ -301,7 +355,21 @@ class SCGiftVapSvgaManager { customResources: customResources, ); + if (_tq.length >= _maxPendingTaskCount && priority <= 0) { + _log( + 'drop play request because queue is full path=$path ' + 'priority=$priority queue=${_tq.length}', + ); + return; + } _tq.add(task); + while (_tq.length > _maxPendingTaskCount) { + final removedTask = _tq.removeLast(); + _log( + 'trim queued task path=${removedTask.path} ' + 'priority=${removedTask.priority} queue=${_tq.length}', + ); + } _log('task enqueued path=$path queueAfter=${_tq.length}'); if (!_play) { _pn(); @@ -486,6 +554,10 @@ class SCGiftVapSvgaManager { _log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}'); _dis = true; stopPlayback(); + _svgaLoadTasks.clear(); + _playablePathTasks.clear(); + videoItemCache.clear(); + _playablePathCache.clear(); _rgc?.dispose(); _rgc = null; _rsc?.dispose(); @@ -564,6 +636,11 @@ class SCPriorityQueue { void clear() => _els.clear(); + E removeLast() { + if (isEmpty) throw StateError("No elements"); + return _els.removeLast(); + } + List get unorderedElements => List.from(_els); // 实现 Iterable 接口 diff --git a/lib/ui_kit/components/sc_page_list.dart b/lib/ui_kit/components/sc_page_list.dart index 73b98ff..c9cf934 100644 --- a/lib/ui_kit/components/sc_page_list.dart +++ b/lib/ui_kit/components/sc_page_list.dart @@ -115,11 +115,17 @@ class SCPageListState extends State { }, child: needLoading && isLoading - ? Center(child: CupertinoActivityIndicator(color: Colors.white24,)) + ? Center( + child: CupertinoActivityIndicator( + color: Colors.white24, + ), + ) : empty(), ) : needLoading && isLoading - ? Center(child: CupertinoActivityIndicator(color: Colors.white24)) + ? Center( + child: CupertinoActivityIndicator(color: Colors.white24), + ) : empty()) : _buildList(), ), @@ -210,4 +216,10 @@ class SCPageListState extends State { ) : Container(); } + + @override + void dispose() { + _refreshController.dispose(); + super.dispose(); + } } diff --git a/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart b/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart index 4d6b265..8650bd2 100644 --- a/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart +++ b/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart @@ -18,6 +18,8 @@ class RoomAnimationQueueScreen extends StatefulWidget { } class _RoomAnimationQueueScreenState extends State { + static const int _maxAnimationQueueLength = 12; + final List _animationQueue = []; bool _isQueueProcessing = false; final Map> _animationKeys = {}; @@ -46,6 +48,7 @@ class _RoomAnimationQueueScreenState extends State { final taskId = DateTime.now().millisecondsSinceEpoch; final animationKey = GlobalKey<_RoomEntranceAnimationState>(); + _trimQueueOverflow(); _animationKeys[taskId] = animationKey; final task = AnimationTask( @@ -77,6 +80,15 @@ class _RoomAnimationQueueScreenState extends State { } } + void _trimQueueOverflow() { + while (_animationQueue.length >= _maxAnimationQueueLength) { + final removeIndex = + _isQueueProcessing && _animationQueue.length > 1 ? 1 : 0; + final removedTask = _animationQueue.removeAt(removeIndex); + _animationKeys.remove(removedTask.id); + } + } + void _startNextAnimation({int retryCount = 0}) { if (_animationQueue.isEmpty) return; @@ -94,7 +106,7 @@ class _RoomAnimationQueueScreenState extends State { }); } else { // 重试多次失败后,跳过当前动画 - print("动画启动失败,跳过当前任务"); + debugPrint("动画启动失败,跳过当前任务"); task.onComplete(); } } diff --git a/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart b/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart index 51baf13..6fa462b 100644 --- a/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart +++ b/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart @@ -29,6 +29,8 @@ class RoomGiftSeatFlightRequest { } class RoomGiftSeatFlightController { + static const int _maxBufferedRequests = 24; + static final RoomGiftSeatFlightController _instance = RoomGiftSeatFlightController._internal(); @@ -46,6 +48,7 @@ class RoomGiftSeatFlightController { } if (_state == null) { + _trimPendingRequestsOverflow(); _pendingRequests.add(normalizedRequest); return; } @@ -69,6 +72,7 @@ class RoomGiftSeatFlightController { if (_state == null) { _trimPendingRequestsForTag(queueTag, maxTrackedRequests); + _trimPendingRequestsOverflow(); _pendingRequests.add(normalizedRequest); return; } @@ -168,6 +172,12 @@ class RoomGiftSeatFlightController { } } + void _trimPendingRequestsOverflow() { + while (_pendingRequests.length >= _maxBufferedRequests) { + _pendingRequests.removeFirst(); + } + } + void _attach(_RoomGiftSeatFlightOverlayState state) { _state = state; while (_pendingRequests.isNotEmpty) { @@ -199,6 +209,8 @@ class RoomGiftSeatFlightOverlay extends StatefulWidget { class _RoomGiftSeatFlightOverlayState extends State with SingleTickerProviderStateMixin { + static const int _maxQueuedRequests = 24; + final Queue<_QueuedRoomGiftSeatFlightRequest> _queue = Queue(); final GlobalKey _overlayKey = GlobalKey(); @@ -240,6 +252,7 @@ class _RoomGiftSeatFlightOverlayState extends State void _enqueue(RoomGiftSeatFlightRequest request) { _ensureCenterVisual(request); + _trimQueuedRequestsOverflow(); _queue.add(_QueuedRoomGiftSeatFlightRequest(request: request)); _scheduleNextAnimation(); } @@ -346,6 +359,12 @@ class _RoomGiftSeatFlightOverlayState extends State return false; } + void _trimQueuedRequestsOverflow() { + while (_queue.length >= _maxQueuedRequests) { + _queue.removeFirst(); + } + } + void _scheduleNextAnimation() { if (_isPlaying || _queue.isEmpty || !mounted) { return; diff --git a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart index 3bfb63a..900fb41 100644 --- a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart @@ -303,6 +303,9 @@ class _FloatingLuckGiftScreenWidgetState ], ), textAlign: TextAlign.start, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, strutStyle: StrutStyle( height: 1.1, fontWeight: FontWeight.bold, diff --git a/lib/ui_kit/widgets/room/seat/room_seat_widget.dart b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart index b1e529a..acfede8 100644 --- a/lib/ui_kit/widgets/room/seat/room_seat_widget.dart +++ b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart @@ -5,25 +5,37 @@ import 'package:yumi/services/audio/rtc_manager.dart'; import '../../../../modules/room/seat/sc_seat_item.dart'; - - class RoomSeatWidget extends StatefulWidget { @override _RoomSeatWidgetState createState() => _RoomSeatWidgetState(); } class _RoomSeatWidgetState extends State { + int _lastSeatCount = 0; + + int _resolvedSeatCount(RtcProvider ref) { + final int seatCount = ref.roomWheatMap.length; + if (!ref.isExitingCurrentVoiceRoomSession && seatCount > 0) { + _lastSeatCount = seatCount; + } + if (ref.isExitingCurrentVoiceRoomSession && _lastSeatCount > 0) { + return _lastSeatCount; + } + return seatCount; + } + @override Widget build(BuildContext context) { return Consumer( builder: (context, ref, child) { - return ref.roomWheatMap.length == 5 + final int seatCount = _resolvedSeatCount(ref); + return seatCount == 5 ? _buildSeat5() - : (ref.roomWheatMap.length == 10 + : (seatCount == 10 ? _buildSeat10() - : (ref.roomWheatMap.length == 15 + : (seatCount == 15 ? _buildSeat15() - : (ref.roomWheatMap.length == 20 + : (seatCount == 20 ? _buildSeat20() : Container(height: 180.w)))); }, diff --git a/lib/ui_kit/widgets/room/switch_model/room_mic_switch_page.dart b/lib/ui_kit/widgets/room/switch_model/room_mic_switch_page.dart index 26ef025..11eba24 100644 --- a/lib/ui_kit/widgets/room/switch_model/room_mic_switch_page.dart +++ b/lib/ui_kit/widgets/room/switch_model/room_mic_switch_page.dart @@ -25,6 +25,12 @@ class _RoomMicSwitchPageState extends State _tabController = TabController(length: _pages.length, vsync: this); } + @override + void dispose() { + _tabController?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { _tabs.clear(); diff --git a/sc_images/index/sc_icon_home_room_rank_border_1.svga b/sc_images/index/sc_icon_home_room_rank_border_1.svga new file mode 100644 index 0000000..1644537 Binary files /dev/null and b/sc_images/index/sc_icon_home_room_rank_border_1.svga differ diff --git a/sc_images/index/sc_icon_home_room_rank_border_2.svga b/sc_images/index/sc_icon_home_room_rank_border_2.svga new file mode 100644 index 0000000..cd5fc03 Binary files /dev/null and b/sc_images/index/sc_icon_home_room_rank_border_2.svga differ diff --git a/sc_images/index/sc_icon_home_room_rank_border_3.svga b/sc_images/index/sc_icon_home_room_rank_border_3.svga new file mode 100644 index 0000000..22fbb2e Binary files /dev/null and b/sc_images/index/sc_icon_home_room_rank_border_3.svga differ diff --git a/sc_images/room/sc_icon_room_chat_tab_all_selected.png b/sc_images/room/sc_icon_room_chat_tab_all_selected.png new file mode 100644 index 0000000..413621a Binary files /dev/null and b/sc_images/room/sc_icon_room_chat_tab_all_selected.png differ diff --git a/sc_images/room/sc_icon_room_chat_tab_all_unselected.png b/sc_images/room/sc_icon_room_chat_tab_all_unselected.png new file mode 100644 index 0000000..581f136 Binary files /dev/null and b/sc_images/room/sc_icon_room_chat_tab_all_unselected.png differ diff --git a/sc_images/room/sc_icon_room_chat_tab_chat_selected.png b/sc_images/room/sc_icon_room_chat_tab_chat_selected.png new file mode 100644 index 0000000..78e33c0 Binary files /dev/null and b/sc_images/room/sc_icon_room_chat_tab_chat_selected.png differ diff --git a/sc_images/room/sc_icon_room_chat_tab_chat_unselected.png b/sc_images/room/sc_icon_room_chat_tab_chat_unselected.png new file mode 100644 index 0000000..46b7476 Binary files /dev/null and b/sc_images/room/sc_icon_room_chat_tab_chat_unselected.png differ diff --git a/sc_images/room/sc_icon_room_chat_tab_gift_selected.png b/sc_images/room/sc_icon_room_chat_tab_gift_selected.png new file mode 100644 index 0000000..a0a9558 Binary files /dev/null and b/sc_images/room/sc_icon_room_chat_tab_gift_selected.png differ diff --git a/sc_images/room/sc_icon_room_chat_tab_gift_unselected.png b/sc_images/room/sc_icon_room_chat_tab_gift_unselected.png new file mode 100644 index 0000000..3b33bcb Binary files /dev/null and b/sc_images/room/sc_icon_room_chat_tab_gift_unselected.png differ diff --git a/需求进度.md b/需求进度.md index 8709186..97a52d4 100644 --- a/需求进度.md +++ b/需求进度.md @@ -20,6 +20,9 @@ - 本轮按需求暂未处理网络链路上的启动等待,例如审核态检查或远端启动页配置请求。 ## 已完成模块 +- 已按 2026-04-20 最新首页视觉需求,为 Party 房间列表前 3 个房卡接入新的本地排名边框 SVGA:桌面“房间排序前三的框”中的 3 份素材已导入工程并挂到首页房卡最上层,仅作用于当前列表前 3 项,其余房卡保持原样;后续又继续为前三房卡底部信息区补了横向与底部安全区,避免外扩边框直接压住国旗、房名和在线人数。同时 `Me` 板块里的 `Recent / Followed` tab 已继续对齐上方首页 tab 的斜体字样式,并移除了点击时的波浪反馈。 +- 已按 2026-04-20 最新房间联调继续修正房主在自己房间里的麦位操作回归:当前房主/管理员点击自己所在麦位时,不再被“直接打开资料卡”逻辑短路,会重新回到底部麦位菜单,从而正常看到自己可用的上下麦/禁麦操作;同时上麦、下麦、禁麦、解禁、锁麦、解锁这些动作在接口成功后会立即回写本地麦位状态,并主动补拉一次最新麦位列表,减少房主端自己操作后 UI 状态延迟或短暂错乱。 +- 已按 2026-04-20 最新确认继续完成 `Wallet -> Recharge` 的 MiFaPay 真链路接入:页面仍保持“原页面结构 + 局部新增”的方案,只在原充值页白色内容区顶部新增 `Recharge methods`,且该标题已改成项目常见的土豪金色;`Google Pay / Apple Pay` 继续保留原生内购列表,`MiFaPay` 已从占位切成真实接口驱动,进入页面会拉 `/order/web/pay/country`、`/order/web/pay/commodity`,底部弹窗会展示真实商品与支付渠道,确认后调用 `/order/web/pay/recharge` 下单并拉起 MiFaPay H5 WebView。用户关闭收银台返回 App 后,当前会立即刷新钱包余额并轮询最新金币额;命中到账后会直接提示购买成功,避免只停留在“支付确认中”。 - 已按 2026-04-18 联调要求继续收口幸运礼物链路:项目默认 API host 已临时切到 `http://192.168.110.43:1100/` 方便直连测试环境;`/gift/give/lucky-gift` 请求体也已补齐为最新结构,除原有 `giftId/quantity/roomId/acceptUserIds/checkCombo` 外,会稳定携带 `gameId: null`、`accepts: []`、`dynamicContentId: null`、`songId: null` 这几组字段,便于和当前后端参数定义保持一致。 - 已继续补齐 `GAME_LUCKY_GIFT` 的 socket 播报与中奖动效:房间群消息收到该类型后,现在会统一落聊天室中奖消息、奖励弹层队列与发送者余额回写,不再只在 `3x+` 时才触发顶部奖励动画;同时全局飘窗已改为按服务端 `globalNews` 字段决定是否展示,避免再用前端本地倍率硬编码去猜。 - 已按最新 UI 口径重排幸运礼物中奖展示:顶部 `LuckGiftNomorAnimWidget` 不再叠加大头像、倍率和奖励框,只在“单个礼物倍率 `> 10x`”或“单次中奖金币 `> 5000`”时负责播全屏 `luck_gift_reward_burst.svga`;原本的 `luck_gift_reward_frame.svga` 已改挂到房间礼物播报条最右侧,并在框内显示 `+formattedAwardAmount`,金额文案直接复用此前大头像下方那一份中奖金币额。 @@ -27,6 +30,8 @@ - 已继续修正幸运礼物中奖播报条右侧奖励框不显示的问题:根因是 `pubspec.yaml` 之前只显式收录了 `sc_images/room/anim/gift/`,但没有把新的 `sc_images/room/anim/luck_gift/` 子目录单独打进 Flutter 资源清单,导致 `luck_gift_reward_frame.svga` 在运行时可引用但未被实际打包;当前已补齐资源目录声明,并为播报条奖励框增加本地 fallback,避免资源异常时再次只剩金额裸字。 - 已按最新文案口径继续收紧幸运礼物中奖消息:聊天室里高倍率幸运礼物提示不再显示冗长的 `Coins / a lucky(magic) gift` 文字,当前已改为直接展示“中奖金额 + 金币图标 + 对应礼物图”,和房间礼物播报条的视觉表达保持一致。 - 已继续微调幸运礼物中奖视觉:房间礼物播报条右侧的 `luck_gift_reward_frame.svga` 已进一步放大,并给右侧区域和主条正文预留了更宽的排版空间,避免资源已经正常显示但因为画布比例和透明边距看起来过小;同时聊天室高亮中奖消息与房间顶部幸运礼物横幅现已统一改成“中奖金额 + 金币图标 + from + 礼物图”的表达,去掉原先 `Coins / a lucky(magic) gift` 这类冗长英文文案;另外全屏 `luck_gift_reward_burst.svga` 也已补上中部金额文案,避免播发时只见特效不见本次实际中奖金币数。 +- 已按 2026-04-20 最新联调继续收口幸运礼物播报噪音:顶部幸运礼物飘屏现在会对同一房间、同一送礼人、同一接收人、同一礼物的连续中奖做队列内聚合,不再每来一条都重新追加一个新飘屏;当前展示中的那一条也会直接叠加金币额并刷新,避免“刷礼过快时飘屏一条接一条排队刷过去”。同时飘屏中奖文案已强制收为单行显示,防止金额、`from` 和礼物图被挤成上下两行。 +- 已继续修复房间礼物播报条偶发出现两条一模一样内容的问题:此前 `GiftAnimationManager` 只会和“正在播放”的同 `labelId` 项做合并,等待队列里的同类播报不会提前去重,因此高频情况下仍可能排出两条完全一样的条目;当前已改为在入队时同时检查“正在播”和“待播”两侧,命中相同播报键时直接原地合并,不再把重复项继续塞进队列。 - 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。 - 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。 - 已继续微调语言房个人卡片与送礼 UI:个人卡片底部动作文案现已支持两行居中展示,避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。 @@ -157,6 +162,14 @@ ## 已改动文件 - `需求进度.md` +- `lib/main.dart` +- `lib/modules/wallet/recharge/recharge_page.dart` +- `lib/modules/wallet/recharge/recharge_method_bottom_sheet.dart` +- `lib/modules/wallet/recharge/mifa_pay_webview_page.dart` +- `lib/services/payment/mifa_pay_manager.dart` +- `lib/shared/business_logic/models/res/sc_mifa_pay_res.dart` +- `lib/shared/business_logic/repositories/config_repository.dart` +- `lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart` - `lib/shared/data_sources/models/enum/sc_gift_type.dart` - `lib/shared/tools/sc_network_image_utils.dart` - `lib/shared/tools/sc_gift_vap_svga_manager.dart` @@ -193,6 +206,9 @@ - `lib/modules/home/popular/mine/sc_home_mine_page.dart` - `lib/ui_kit/widgets/room/room_live_audio_indicator.dart` - `lib/modules/search/sc_search_page.dart` +- `sc_images/index/sc_icon_home_room_rank_border_1.svga` +- `sc_images/index/sc_icon_home_room_rank_border_2.svga` +- `sc_images/index/sc_icon_home_room_rank_border_3.svga` - `lib/modules/store/headdress/store_headdress_page.dart` - `lib/modules/store/mountains/store_mountains_page.dart` - `lib/modules/store/theme/store_theme_page.dart`