UI更新 内存泄露优化 初步对接mifapay

This commit is contained in:
NIGGER SLAYER 2026-04-20 18:52:03 +08:00
parent f78c099a47
commit 5b607800be
54 changed files with 5455 additions and 2133 deletions

View File

@ -34,6 +34,7 @@ import 'services/auth/authentication_manager.dart';
import 'services/gift/gift_animation_manager.dart'; import 'services/gift/gift_animation_manager.dart';
import 'services/gift/gift_system_manager.dart'; import 'services/gift/gift_system_manager.dart';
import 'services/payment/google_payment_manager.dart'; import 'services/payment/google_payment_manager.dart';
import 'services/payment/mifa_pay_manager.dart';
import 'services/localization/localization_manager.dart'; import 'services/localization/localization_manager.dart';
import 'services/room/rc_room_manager.dart'; import 'services/room/rc_room_manager.dart';
import 'services/audio/rtc_manager.dart'; import 'services/audio/rtc_manager.dart';
@ -261,6 +262,10 @@ class RootAppWithProviders extends StatelessWidget {
lazy: true, lazy: true,
create: (context) => IOSPaymentProcessor(), create: (context) => IOSPaymentProcessor(),
), ),
ChangeNotifierProvider<MiFaPayManager>(
lazy: true,
create: (context) => MiFaPayManager(),
),
ChangeNotifierProvider<ShopManager>( ChangeNotifierProvider<ShopManager>(
lazy: true, lazy: true,
create: (context) => ShopManager(), create: (context) => ShopManager(),
@ -408,8 +413,17 @@ class _YumiApplicationState extends State<YumiApplication> {
children: [ children: [
child ?? const SizedBox.shrink(), child ?? const SizedBox.shrink(),
if (SCGlobalConfig.allowsHighCostAnimations) if (SCGlobalConfig.allowsHighCostAnimations)
const Positioned.fill( Consumer<RtcProvider>(
child: VapPlusSvgaPlayer(tag: "room_gift"), builder: (context, rtcProvider, _) {
if (!rtcProvider.roomVisualEffectsEnabled) {
return const SizedBox.shrink();
}
return const Positioned.fill(
child: VapPlusSvgaPlayer(
tag: "room_gift",
),
);
},
), ),
Positioned.fill( Positioned.fill(
child: RoomGiftSeatFlightOverlay( child: RoomGiftSeatFlightOverlay(

View File

@ -55,7 +55,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(widget.type); final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(
widget.type,
);
if (violationTypeMapping.isNotEmpty) { if (violationTypeMapping.isNotEmpty) {
// //
violationType = violationTypeMapping.values.first; violationType = violationTypeMapping.values.first;
@ -65,6 +67,12 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
imageUrls = List.generate(maxImageCount, (index) => ''); imageUrls = List.generate(maxImageCount, (index) => '');
} }
@override
void dispose() {
_descriptionController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
@ -166,8 +174,14 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
(widget.userProfile (widget.userProfile
?.hasSpecialId() ?? ?.hasSpecialId() ??
false) false)
? _strategy.getAdminEditingIcon('specialIdBg') ? _strategy
: _strategy.getAdminEditingIcon('normalIdBg'), .getAdminEditingIcon(
'specialIdBg',
)
: _strategy
.getAdminEditingIcon(
'normalIdBg',
),
), ),
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
), ),
@ -185,7 +199,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
), ),
SizedBox(width: 5.w), SizedBox(width: 5.w),
Image.asset( Image.asset(
_strategy.getAdminEditingIcon('copyId'), _strategy.getAdminEditingIcon(
'copyId',
),
width: 12.w, width: 12.w,
height: 12.w, height: 12.w,
), ),
@ -281,7 +297,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
), ),
SizedBox(width: 5.w), SizedBox(width: 5.w),
Image.asset( Image.asset(
_strategy.getAdminEditingIcon('copyId'), _strategy.getAdminEditingIcon(
'copyId',
),
width: 12.w, width: 12.w,
height: 12.w, height: 12.w,
color: Colors.black26, color: Colors.black26,
@ -343,7 +361,7 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
children: [ children: [
// //
..._buildViolationTypeOptions(context), ..._buildViolationTypeOptions(context),
SizedBox(height: 20.w,), SizedBox(height: 20.w),
Row( Row(
children: [ children: [
text( text(
@ -358,11 +376,13 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
SizedBox(height: 8.w), SizedBox(height: 8.w),
Container( Container(
padding: EdgeInsets.all(5.w), padding: EdgeInsets.all(5.w),
decoration: _strategy.getAdminEditingInputDecoration(), decoration:
_strategy.getAdminEditingInputDecoration(),
child: TextField( child: TextField(
controller: _descriptionController, controller: _descriptionController,
onChanged: (text) {}, onChanged: (text) {},
maxLength: _strategy.getAdminEditingDescriptionMaxLength(), maxLength:
_strategy.getAdminEditingDescriptionMaxLength(),
maxLines: 5, maxLines: 5,
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText:
@ -413,52 +433,75 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
Row( Row(
children: [ children: [
// //
...List.generate(_strategy.getAdminEditingMaxImageUploadCount(), (index) { ...List.generate(
return Expanded( _strategy
child: GestureDetector( .getAdminEditingMaxImageUploadCount(),
child: Stack( (index) {
children: [ return Expanded(
imageUrls[index].isNotEmpty child: GestureDetector(
? netImage(url: imageUrls[index], height: 100.w) child: Stack(
: Image.asset( children: [
_strategy.getAdminEditingIcon('addPic'), imageUrls[index].isNotEmpty
height: 100.w, ? netImage(
), url: imageUrls[index],
imageUrls[index].isNotEmpty height: 100.w,
? Positioned( )
top: 5.w, : Image.asset(
right: 5.w, _strategy.getAdminEditingIcon(
child: GestureDetector( 'addPic',
child: Image.asset( ),
_strategy.getAdminEditingIcon('closePic'), height: 100.w,
width: 14.w, ),
height: 14.w, imageUrls[index].isNotEmpty
), ? Positioned(
onTap: () { top: 5.w,
setState(() { right: 5.w,
imageUrls[index] = ""; child: GestureDetector(
}); child: Image.asset(
}, _strategy
), .getAdminEditingIcon(
) 'closePic',
: Container(), ),
], width: 14.w,
), height: 14.w,
onTap: () { ),
SCPickUtils.pickImage(context, ( onTap: () {
bool success, setState(() {
String url, imageUrls[index] = "";
) { });
if (success) { },
setState(() { ),
imageUrls[index] = url; )
}); : Container(),
} ],
}); ),
onTap: () {
SCPickUtils.pickImage(context, (
bool success,
String url,
) {
if (success) {
setState(() {
imageUrls[index] = url;
});
}
});
},
),
);
}, },
), )
); .expand(
}).expand((widget) => [widget, if (_strategy.getAdminEditingMaxImageUploadCount() > 1 && widget != Expanded) SizedBox(width: 5.w)]).toList(), (widget) => [
widget,
if (_strategy
.getAdminEditingMaxImageUploadCount() >
1 &&
widget != Expanded)
SizedBox(width: 5.w),
],
)
.toList(),
], ],
), ),
SizedBox(height: 15.w), SizedBox(height: 15.w),
@ -472,33 +515,44 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
debouncer.debounce( debouncer.debounce(
duration: Duration(milliseconds: 350), duration: Duration(milliseconds: 350),
onDebounce: () { onDebounce: () {
List<String> uploadedImages = imageUrls.where((url) => url.isNotEmpty).toList(); List<String> uploadedImages =
imageUrls.where((url) => url.isNotEmpty).toList();
if (widget.type == "User") { if (widget.type == "User") {
SCAccountRepository().userViolationHandle( SCAccountRepository()
widget.userProfile?.id ?? "", .userViolationHandle(
violationType, widget.userProfile?.id ?? "",
1, violationType,
_descriptionController.text, 1,
imageUrls: imageUrls, _descriptionController.text,
).then((b){ imageUrls: imageUrls,
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); )
SCNavigatorUtils.goBack(context); .then((b) {
}).catchError((e){ SCTts.show(
SCAppLocalizations.of(
}); context,
)!.operationSuccessful,
);
SCNavigatorUtils.goBack(context);
})
.catchError((e) {});
} else { } else {
SCChatRoomRepository().roomViolationHandle( SCChatRoomRepository()
widget.roomProfile?.id ?? "", .roomViolationHandle(
violationType, widget.roomProfile?.id ?? "",
1, violationType,
_descriptionController.text, 1,
imageUrls: imageUrls, _descriptionController.text,
).then((b){ imageUrls: imageUrls,
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); )
SCNavigatorUtils.goBack(context); .then((b) {
}).catchError((e){ SCTts.show(
SCAppLocalizations.of(
}); context,
)!.operationSuccessful,
);
SCNavigatorUtils.goBack(context);
})
.catchError((e) {});
} }
}, },
); );
@ -512,7 +566,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
gradient: LinearGradient( gradient: LinearGradient(
begin: AlignmentDirectional.topCenter, begin: AlignmentDirectional.topCenter,
end: AlignmentDirectional.bottomCenter, end: AlignmentDirectional.bottomCenter,
colors: _strategy.getAdminEditingButtonGradient('warning'), colors: _strategy.getAdminEditingButtonGradient(
'warning',
),
), ),
borderRadius: BorderRadius.circular(25.w), borderRadius: BorderRadius.circular(25.w),
), ),
@ -533,33 +589,44 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
debouncer.debounce( debouncer.debounce(
duration: Duration(milliseconds: 350), duration: Duration(milliseconds: 350),
onDebounce: () { onDebounce: () {
List<String> uploadedImages = imageUrls.where((url) => url.isNotEmpty).toList(); List<String> uploadedImages =
imageUrls.where((url) => url.isNotEmpty).toList();
if (widget.type == "User") { if (widget.type == "User") {
SCAccountRepository().userViolationHandle( SCAccountRepository()
widget.userProfile?.id ?? "", .userViolationHandle(
violationType, widget.userProfile?.id ?? "",
2, violationType,
_descriptionController.text, 2,
imageUrls: imageUrls, _descriptionController.text,
).then((b){ imageUrls: imageUrls,
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); )
SCNavigatorUtils.goBack(context); .then((b) {
}).catchError((e){ SCTts.show(
SCAppLocalizations.of(
}); context,
)!.operationSuccessful,
);
SCNavigatorUtils.goBack(context);
})
.catchError((e) {});
} else { } else {
SCChatRoomRepository().roomViolationHandle( SCChatRoomRepository()
widget.roomProfile?.id ?? "", .roomViolationHandle(
violationType, widget.roomProfile?.id ?? "",
2, violationType,
_descriptionController.text, 2,
imageUrls: imageUrls, _descriptionController.text,
).then((b){ imageUrls: imageUrls,
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); )
SCNavigatorUtils.goBack(context); .then((b) {
}).catchError((e){ SCTts.show(
SCAppLocalizations.of(
}); context,
)!.operationSuccessful,
);
SCNavigatorUtils.goBack(context);
})
.catchError((e) {});
} }
}, },
); );
@ -573,7 +640,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
gradient: LinearGradient( gradient: LinearGradient(
begin: AlignmentDirectional.topCenter, begin: AlignmentDirectional.topCenter,
end: AlignmentDirectional.bottomCenter, end: AlignmentDirectional.bottomCenter,
colors: _strategy.getAdminEditingButtonGradient('adjust'), colors: _strategy.getAdminEditingButtonGradient(
'adjust',
),
), ),
borderRadius: BorderRadius.circular(25.w), borderRadius: BorderRadius.circular(25.w),
), ),
@ -598,17 +667,23 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
} }
List<Widget> _buildViolationTypeOptions(BuildContext context) { List<Widget> _buildViolationTypeOptions(BuildContext context) {
final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(widget.type); final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(
widget.type,
);
final List<Widget> options = []; final List<Widget> options = [];
if (violationTypeMapping.isEmpty) { if (violationTypeMapping.isEmpty) {
// //
if (widget.type == "User") { if (widget.type == "User") {
options.add(_item(SCAppLocalizations.of(context)!.userName, 0, 1)); 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 { } else {
options.add(_item(SCAppLocalizations.of(context)!.roomName, 0, 3)); 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)!.roomNotice, 2, 5));
options.add(_item(SCAppLocalizations.of(context)!.roomTheme, 3, 6)); options.add(_item(SCAppLocalizations.of(context)!.roomTheme, 3, 6));
} }
@ -677,7 +752,12 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
text(str, fontSize: 13.sp, textColor: Colors.black,fontWeight: FontWeight.w500), text(
str,
fontSize: 13.sp,
textColor: Colors.black,
fontWeight: FontWeight.w500,
),
Spacer(), Spacer(),
selectedIndex == index selectedIndex == index
? Image.asset( ? Image.asset(

View File

@ -40,6 +40,12 @@ class _SCEditRoomSearchAdminPageState extends State<SCEditRoomSearchAdminPage>
super.initState(); super.initState();
} }
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
@ -79,17 +85,25 @@ class _SCEditRoomSearchAdminPageState extends State<SCEditRoomSearchAdminPage>
child: searchWidget( child: searchWidget(
hint: SCAppLocalizations.of(context)!.enterTheRoomId, hint: SCAppLocalizations.of(context)!.enterTheRoomId,
controller: _textEditingController, controller: _textEditingController,
borderColor: _strategy.getAdminSearchInputBorderColor('roomSearch'), borderColor: _strategy.getAdminSearchInputBorderColor(
textColor: _strategy.getAdminSearchInputTextColor('roomSearch'), 'roomSearch',
),
textColor: _strategy.getAdminSearchInputTextColor(
'roomSearch',
),
), ),
), ),
socialchatGradientButton( socialchatGradientButton(
text: SCAppLocalizations.of(context)!.search, text: SCAppLocalizations.of(context)!.search,
radius: 25, radius: 25,
textSize: 14.sp, textSize: 14.sp,
textColor: _strategy.getAdminSearchButtonTextColor('roomSearch'), textColor: _strategy.getAdminSearchButtonTextColor(
'roomSearch',
),
gradient: LinearGradient( gradient: LinearGradient(
colors: _strategy.getAdminSearchButtonGradient('roomSearch'), colors: _strategy.getAdminSearchButtonGradient(
'roomSearch',
),
begin: Alignment.centerLeft, begin: Alignment.centerLeft,
end: Alignment.centerRight, end: Alignment.centerRight,
), ),

View File

@ -42,6 +42,12 @@ class _SCEditUserSearchAdminPageState extends State<SCEditUserSearchAdminPage>
super.initState(); super.initState();
} }
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
@ -81,17 +87,25 @@ class _SCEditUserSearchAdminPageState extends State<SCEditUserSearchAdminPage>
child: searchWidget( child: searchWidget(
hint: SCAppLocalizations.of(context)!.enterTheUserId, hint: SCAppLocalizations.of(context)!.enterTheUserId,
controller: _textEditingController, controller: _textEditingController,
borderColor: _strategy.getAdminSearchInputBorderColor('userSearch'), borderColor: _strategy.getAdminSearchInputBorderColor(
textColor: _strategy.getAdminSearchInputTextColor('userSearch'), 'userSearch',
),
textColor: _strategy.getAdminSearchInputTextColor(
'userSearch',
),
), ),
), ),
socialchatGradientButton( socialchatGradientButton(
text: SCAppLocalizations.of(context)!.search, text: SCAppLocalizations.of(context)!.search,
radius: 25, radius: 25,
textSize: 14.sp, textSize: 14.sp,
textColor: _strategy.getAdminSearchButtonTextColor('userSearch'), textColor: _strategy.getAdminSearchButtonTextColor(
'userSearch',
),
gradient: LinearGradient( gradient: LinearGradient(
colors: _strategy.getAdminSearchButtonGradient('userSearch'), colors: _strategy.getAdminSearchButtonGradient(
'userSearch',
),
begin: Alignment.centerLeft, begin: Alignment.centerLeft,
end: Alignment.centerRight, end: Alignment.centerRight,
), ),
@ -190,8 +204,12 @@ class _SCEditUserSearchAdminPageState extends State<SCEditUserSearchAdminPage>
image: DecorationImage( image: DecorationImage(
image: AssetImage( image: AssetImage(
(data.hasSpecialId() ?? false) (data.hasSpecialId() ?? false)
? _strategy.getAdminSearchUserInfoIcon('specialIdBg') ? _strategy.getAdminSearchUserInfoIcon(
: _strategy.getAdminSearchUserInfoIcon('normalIdBg'), 'specialIdBg',
)
: _strategy.getAdminSearchUserInfoIcon(
'normalIdBg',
),
), ),
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
), ),

View File

@ -63,6 +63,13 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
passController.text = pwd; passController.text = pwd;
} }
@override
void dispose() {
accountController.dispose();
passController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final businessLogicStrategy = SCGlobalConfig.businessLogicStrategy; final businessLogicStrategy = SCGlobalConfig.businessLogicStrategy;

View File

@ -138,6 +138,10 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
rtmProvider?.onMessageRecvC2CReadListener = null; rtmProvider?.onMessageRecvC2CReadListener = null;
rtmProvider?.onRevokeMessageListener = null; rtmProvider?.onRevokeMessageListener = null;
rtmProvider?.onNewMessageCurrentConversationListener = null; rtmProvider?.onNewMessageCurrentConversationListener = null;
_textController.dispose();
_scrollController.dispose();
_refreshController.dispose();
_focusNode.dispose();
super.dispose(); super.dispose();
} }
@ -281,7 +285,9 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
} }
void loadFriend() { void loadFriend() {
if (SCGlobalConfig.isSystemConversationId(currentConversation?.conversationID) || if (SCGlobalConfig.isSystemConversationId(
currentConversation?.conversationID,
) ||
SCGlobalConfig.isSystemUserId(currentConversation?.userID)) { SCGlobalConfig.isSystemUserId(currentConversation?.userID)) {
return; return;
} }

View File

@ -30,6 +30,12 @@ class _HomeEventPageState extends State<HomeEventPage>
_loadData(); _loadData();
} }
@override
void dispose() {
_refreshController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -50,7 +56,7 @@ class _HomeEventPageState extends State<HomeEventPage>
), ),
], ],
), ),
SizedBox(height: 8.w,), SizedBox(height: 8.w),
Row( Row(
children: [ children: [
SizedBox(width: 20.w), SizedBox(width: 20.w),

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
@ -22,17 +24,49 @@ class SCRoomFollowPage extends SCPageList {
} }
class _RoomFollowPageState class _RoomFollowPageState
extends SCPageListState<FollowRoomRes, SCRoomFollowPage> { extends SCPageListState<FollowRoomRes, SCRoomFollowPage>
with WidgetsBindingObserver {
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
String? lastId; String? lastId;
Timer? _roomListRefreshTimer;
bool _isSilentRefreshingRooms = false;
bool _wasTickerModeEnabled = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
enablePullUp = true; enablePullUp = true;
backgroundColor = Colors.transparent; backgroundColor = Colors.transparent;
isGridView = true; isGridView = true;
gridViewCount = 2; gridViewCount = 2;
loadData(1); 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 @override
@ -194,7 +228,7 @@ class _RoomFollowPageState
?.isEmpty ?? ?.isEmpty ??
false) false)
? text( ? text(
roomRes.roomProfile?.extValues?.memberQuantity ?? "0", roomRes.roomProfile?.displayMemberCount ?? "0",
fontSize: 10.sp, fontSize: 10.sp,
lineHeight: 1, 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<FollowRoomRes> previous, List<FollowRoomRes> 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<FollowRoomRes> 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<void> _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;
}
}
} }

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
@ -22,17 +24,49 @@ class SCRoomHistoryPage extends SCPageList {
} }
class _SCRoomHistoryPageState class _SCRoomHistoryPageState
extends SCPageListState<FollowRoomRes, SCRoomHistoryPage> { extends SCPageListState<FollowRoomRes, SCRoomHistoryPage>
with WidgetsBindingObserver {
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
String? lastId; String? lastId;
Timer? _roomListRefreshTimer;
bool _isSilentRefreshingRooms = false;
bool _wasTickerModeEnabled = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
enablePullUp = true; enablePullUp = true;
backgroundColor = Colors.transparent; backgroundColor = Colors.transparent;
isGridView = true; isGridView = true;
gridViewCount = 2; gridViewCount = 2;
loadData(1); 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 @override
@ -195,7 +229,7 @@ class _SCRoomHistoryPageState
?.isEmpty ?? ?.isEmpty ??
false) false)
? text( ? text(
roomRes.roomProfile?.extValues?.memberQuantity ?? "0", roomRes.roomProfile?.displayMemberCount ?? "0",
fontSize: 10.sp, fontSize: 10.sp,
lineHeight: 1, 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<FollowRoomRes> previous, List<FollowRoomRes> 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<FollowRoomRes> 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<void> _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;
}
}
} }

View File

@ -85,17 +85,18 @@ class _HomeMinePageState extends State<SCHomeMinePage>
labelPadding: EdgeInsets.symmetric(horizontal: 12.w), labelPadding: EdgeInsets.symmetric(horizontal: 12.w),
labelColor: SocialChatTheme.primaryLight, labelColor: SocialChatTheme.primaryLight,
isScrollable: true, isScrollable: true,
splashFactory: NoSplash.splashFactory,
overlayColor: WidgetStateProperty.all(Colors.transparent),
indicator: BoxDecoration(), indicator: BoxDecoration(),
unselectedLabelColor: Colors.white, unselectedLabelColor: Colors.white,
labelStyle: TextStyle( labelStyle: TextStyle(
fontSize: 15.sp, fontWeight: FontWeight.bold,
fontFamily: 'MyCustomFont', fontStyle: FontStyle.italic,
fontWeight: FontWeight.w600, fontSize: 19.sp,
), ),
unselectedLabelStyle: TextStyle( unselectedLabelStyle: TextStyle(
fontSize: 13.sp, fontWeight: FontWeight.normal,
fontFamily: 'MyCustomFont', fontSize: 14.sp,
fontWeight: FontWeight.w500,
), ),
indicatorColor: Colors.transparent, indicatorColor: Colors.transparent,
dividerColor: Colors.transparent, dividerColor: Colors.transparent,

View File

@ -1,4 +1,5 @@
import 'package:carousel_slider/carousel_slider.dart'; import 'package:carousel_slider/carousel_slider.dart';
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
@ -17,6 +18,7 @@ import '../../../../services/general/sc_app_general_manager.dart';
import '../../../../ui_kit/components/sc_compontent.dart'; import '../../../../ui_kit/components/sc_compontent.dart';
import '../../../../ui_kit/components/text/sc_text.dart'; import '../../../../ui_kit/components/text/sc_text.dart';
import '../../../../ui_kit/widgets/room/room_live_audio_indicator.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'; import '../../../index/main_route.dart';
const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450); const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450);
@ -34,7 +36,16 @@ class SCHomePartyPage extends StatefulWidget {
} }
class _HomePartyPageState extends State<SCHomePartyPage> class _HomePartyPageState extends State<SCHomePartyPage>
with SingleTickerProviderStateMixin { 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<String> _topRankBorderAssets = <String>[
"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<FollowRoomRes> historyRooms = []; List<FollowRoomRes> historyRooms = [];
String? lastId; String? lastId;
bool isLoading = false; bool isLoading = false;
@ -46,24 +57,50 @@ class _HomePartyPageState extends State<SCHomePartyPage>
late final AnimationController _skeletonController; late final AnimationController _skeletonController;
List<SocialChatRoomRes> rooms = []; List<SocialChatRoomRes> rooms = [];
int _currentIndex = 0; int _currentIndex = 0;
Timer? _roomListRefreshTimer;
bool _isSilentRefreshingRooms = false;
bool _isHydratingVisibleRoomCounters = false;
bool _wasTickerModeEnabled = false;
DateTime? _lastRoomCounterHydrationAt;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
_skeletonController = AnimationController( _skeletonController = AnimationController(
vsync: this, vsync: this,
duration: _kPartySkeletonAnimationDuration, duration: _kPartySkeletonAnimationDuration,
)..repeat(); )..repeat();
loadData(); loadData();
_startRoomListAutoRefresh();
} }
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this);
_roomListRefreshTimer?.cancel();
_skeletonController.dispose(); _skeletonController.dispose();
_refreshController.dispose(); _refreshController.dispose();
super.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -121,6 +158,170 @@ class _HomePartyPageState extends State<SCHomePartyPage>
); );
} }
void _startRoomListAutoRefresh() {
_roomListRefreshTimer?.cancel();
_roomListRefreshTimer = Timer.periodic(_roomListRefreshInterval, (_) {
_refreshRoomsSilently();
});
}
bool _canRefreshRoomListSilently() {
return mounted &&
TickerMode.valuesOf(context).enabled &&
!_isSilentRefreshingRooms &&
!isLoading;
}
bool _sameRooms(
List<SocialChatRoomRes> previous,
List<SocialChatRoomRes> next,
) {
if (previous.length != next.length) {
return false;
}
for (int i = 0; i < previous.length; i++) {
if (!_sameRoom(previous[i], next[i])) {
return false;
}
}
return true;
}
bool _sameRoom(SocialChatRoomRes previous, SocialChatRoomRes next) {
return previous.id == next.id &&
previous.roomName == next.roomName &&
previous.roomCover == next.roomCover &&
previous.roomGameIcon == next.roomGameIcon &&
previous.displayMemberCount == next.displayMemberCount &&
previous.extValues?.roomSetting?.password ==
next.extValues?.roomSetting?.password;
}
List<SocialChatRoomRes> _mergeLatestRoomsWithCurrentCounters(
List<SocialChatRoomRes> latestRooms,
) {
if (rooms.isEmpty) {
return latestRooms;
}
final currentRoomById = {
for (final room in rooms)
if ((room.id ?? "").isNotEmpty) room.id!: room,
};
return latestRooms.map((room) {
final roomId = room.id;
if ((roomId ?? "").isEmpty) {
return room;
}
final currentRoom = currentRoomById[roomId];
if (currentRoom == null || room.roomCounter != null) {
return room;
}
return room.copyWith(roomCounter: currentRoom.roomCounter);
}).toList();
}
Future<void> _syncVisibleRoomMemberCounts({bool force = false}) async {
if (!mounted || rooms.isEmpty || _isHydratingVisibleRoomCounters) {
return;
}
final now = DateTime.now();
if (!force &&
_lastRoomCounterHydrationAt != null &&
now.difference(_lastRoomCounterHydrationAt!) <
_roomCounterHydrationMinGap) {
return;
}
final targetRoomIds =
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<String, RoomMemberCounter?>(roomId, null);
}
}),
);
if (!mounted) {
return;
}
final roomIndexById = {
for (int i = 0; i < rooms.length; i++)
if ((rooms[i].id ?? "").isNotEmpty) rooms[i].id!: i,
};
final nextRooms = List<SocialChatRoomRes>.from(rooms);
bool changed = false;
for (final entry in hydratedCounters) {
final roomIndex = roomIndexById[entry.key];
final roomCounter = entry.value;
if (roomIndex == null || roomCounter == null) {
continue;
}
final currentRoom = nextRooms[roomIndex];
final nextRoom = currentRoom.copyWith(roomCounter: roomCounter);
if (_sameRoom(currentRoom, nextRoom)) {
continue;
}
nextRooms[roomIndex] = nextRoom;
changed = true;
}
if (changed) {
setState(() {
rooms = nextRooms;
});
}
} finally {
_isHydratingVisibleRoomCounters = false;
}
}
Future<void> _refreshRoomsSilently() async {
if (!_canRefreshRoomListSilently()) {
return;
}
_isSilentRefreshingRooms = true;
try {
final latestRooms = await SCChatRoomRepository().discovery(
allRegion: true,
);
final mergedRooms = _mergeLatestRoomsWithCurrentCounters(latestRooms);
if (!mounted || !TickerMode.valuesOf(context).enabled) {
return;
}
if (_sameRooms(rooms, mergedRooms)) {
unawaited(_syncVisibleRoomMemberCounts());
return;
}
setState(() {
rooms = mergedRooms;
});
unawaited(_syncVisibleRoomMemberCounts(force: true));
} catch (_) {
} finally {
_isSilentRefreshingRooms = false;
}
}
_banner(SCAppGeneralManager ref) { _banner(SCAppGeneralManager ref) {
final banners = _partyBanners(ref); final banners = _partyBanners(ref);
final hasMultipleBanners = banners.length > 1; final hasMultipleBanners = banners.length > 1;
@ -774,11 +975,14 @@ class _HomePartyPageState extends State<SCHomePartyPage>
SCChatRoomRepository() SCChatRoomRepository()
.discovery(allRegion: true) .discovery(allRegion: true)
.then((values) { .then((values) {
rooms = values; rooms = _mergeLatestRoomsWithCurrentCounters(values);
isLoading = false; isLoading = false;
_refreshController.refreshCompleted(); _refreshController.refreshCompleted();
_refreshController.loadComplete(); _refreshController.loadComplete();
if (mounted) setState(() {}); if (mounted) {
setState(() {});
unawaited(_syncVisibleRoomMemberCounts(force: true));
}
}) })
.catchError((e) { .catchError((e) {
_refreshController.loadNoData(); _refreshController.loadNoData();
@ -805,6 +1009,11 @@ class _HomePartyPageState extends State<SCHomePartyPage>
} }
_buildItem(SocialChatRoomRes res, int index) { _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( return SCDebounceWidget(
child: Container( child: Container(
margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
@ -813,6 +1022,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
color: Colors.transparent, color: Colors.transparent,
), ),
child: Stack( child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
Container( Container(
@ -831,104 +1041,125 @@ class _HomePartyPageState extends State<SCHomePartyPage>
height: 200.w, height: 200.w,
), ),
), ),
Container( Padding(
padding: EdgeInsets.symmetric(vertical: 6.w), padding: EdgeInsets.only(
margin: EdgeInsets.symmetric(horizontal: 1.w), left: rankInfoHorizontalInset,
decoration: BoxDecoration( right: rankInfoHorizontalInset,
image: DecorationImage( bottom: rankInfoBottomInset,
image: AssetImage(
"sc_images/index/sc_icon_index_room_brd.png",
),
fit: BoxFit.fill,
),
), ),
child: Row( child: Container(
children: [ padding: EdgeInsets.symmetric(vertical: 6.w),
SizedBox(width: 10.w), margin: EdgeInsets.symmetric(horizontal: 1.w),
Consumer<SCAppGeneralManager>( decoration: BoxDecoration(
builder: (_, provider, __) { image: DecorationImage(
return netImage( image: AssetImage(
url: "sc_images/index/sc_icon_index_room_brd.png",
"${provider.findCountryByName(res.countryName ?? "")?.nationalFlag}", ),
width: 20.w, fit: BoxFit.fill,
height: 13.w,
borderRadius: BorderRadius.circular(2.w),
);
},
), ),
SizedBox(width: 5.w), ),
Expanded( child: Row(
child: SizedBox( children: [
height: 17.w, SizedBox(width: 10.w),
child: Align( Consumer<SCAppGeneralManager>(
alignment: Alignment.centerLeft, builder: (_, provider, __) {
child: Transform.translate( return netImage(
offset: Offset(0, -0.6.w), url:
child: text( "${provider.findCountryByName(res.countryName ?? "")?.nationalFlag}",
res.roomName ?? "", width: 20.w,
fontSize: 13.sp, height: 13.w,
textColor: Color(0xffffffff), borderRadius: BorderRadius.circular(2.w),
fontWeight: FontWeight.w400, );
lineHeight: 1, },
),
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,
// ),
// ),
), ),
// (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),
SizedBox(width: 5.w), (res.extValues?.roomSetting?.password?.isEmpty ?? false)
(res.extValues?.roomSetting?.password?.isEmpty ?? false) ? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w) : Image.asset(
: Image.asset( "sc_images/index/sc_icon_room_suo.png",
"sc_images/index/sc_icon_room_suo.png", width: 20.w,
width: 20.w, height: 20.w,
height: 20.w, ),
), (res.extValues?.roomSetting?.password?.isEmpty ?? false)
(res.extValues?.roomSetting?.password?.isEmpty ?? false) ? SizedBox(width: 3.w)
? SizedBox(width: 3.w) : Container(height: 10.w),
: Container(height: 10.w), (res.extValues?.roomSetting?.password?.isEmpty ?? false)
(res.extValues?.roomSetting?.password?.isEmpty ?? false) ? text(
? text( res.displayMemberCount,
res.extValues?.memberQuantity ?? "0", fontSize: 10.sp,
fontSize: 10.sp, lineHeight: 1,
lineHeight: 1, )
) : Container(height: 10.w),
: Container(height: 10.w), SizedBox(width: 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,
),
),
], ],
), ),
), ),

View File

@ -36,333 +36,384 @@ class _ReportPageState extends State<ReportPage> {
String pic03 = ""; String pic03 = "";
final TextEditingController _descriptionController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController();
@override
void dispose() {
_descriptionController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SCDebounceWidget(child: Stack( return SCDebounceWidget(
children: [ child: Stack(
Image.asset( children: [
SCGlobalConfig.businessLogicStrategy.getReportPageBackgroundImage(), Image.asset(
width: ScreenUtil().screenWidth, SCGlobalConfig.businessLogicStrategy.getReportPageBackgroundImage(),
height: ScreenUtil().screenHeight, width: ScreenUtil().screenWidth,
fit: BoxFit.fill, height: ScreenUtil().screenHeight,
), fit: BoxFit.fill,
Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
appBar: SocialChatStandardAppBar(
actions: [],
title: SCAppLocalizations.of(context)!.report,
backgroundColor: Colors.transparent,
), ),
body: SafeArea( Scaffold(
top: false, backgroundColor: Colors.transparent,
child: SingleChildScrollView( resizeToAvoidBottomInset: false,
child: Column( appBar: SocialChatStandardAppBar(
crossAxisAlignment: CrossAxisAlignment.start, actions: [],
children: [ title: SCAppLocalizations.of(context)!.report,
Row( backgroundColor: Colors.transparent,
children: [ ),
SizedBox(width: 15.w), body: SafeArea(
text( top: false,
SCAppLocalizations.of( child: SingleChildScrollView(
context, child: Column(
)!.pleaseSelectTheTypeContent, crossAxisAlignment: CrossAxisAlignment.start,
fontSize: 14.sp, children: [
textColor: SCGlobalConfig.businessLogicStrategy.getReportPageHintTextColor(), Row(
),
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(
children: [ children: [
_item(SCAppLocalizations.of(context)!.spam, 3), SizedBox(width: 15.w),
_item(SCAppLocalizations.of(context)!.fraud, 4), text(
_item( SCAppLocalizations.of(
SCAppLocalizations.of(context)!.maliciousHarassment, context,
0, )!.pleaseSelectTheTypeContent,
fontSize: 14.sp,
textColor:
SCGlobalConfig.businessLogicStrategy
.getReportPageHintTextColor(),
), ),
_item(SCAppLocalizations.of(context)!.other, 1), SizedBox(width: 15.w),
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),
], ],
), ),
), Container(
SizedBox(height: 45.w), margin: EdgeInsets.only(
SCDebounceWidget( top: 10.w,
onTap: () async { left: 15.w,
SCLoadingManager.show(); right: 15.w,
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( padding: EdgeInsets.symmetric(horizontal: 15.w),
SCAppLocalizations.of(context)!.submit, child: Column(
style: TextStyle(color: SCGlobalConfig.businessLogicStrategy.getReportPageSubmitButtonTextColor(), fontSize: 16.sp), 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: 45.w),
SizedBox(height: 35.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: (){ onTap: () {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}); },
);
} }
Widget _item(String str, int index) { Widget _item(String str, int index) {
@ -381,11 +432,19 @@ class _ReportPageState extends State<ReportPage> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
text(str, fontSize: 14.sp, textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor()), text(
str,
fontSize: 14.sp,
textColor:
SCGlobalConfig.businessLogicStrategy
.getReportPagePrimaryTextColor(),
),
Spacer(), Spacer(),
selectedIndex == index selectedIndex == index
? Image.asset( ? Image.asset(
SCGlobalConfig.businessLogicStrategy.getReportPageIcon('checked'), SCGlobalConfig.businessLogicStrategy.getReportPageIcon(
'checked',
),
width: 20.w, width: 20.w,
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
) )
@ -394,7 +453,12 @@ class _ReportPageState extends State<ReportPage> {
height: 20.w, height: 20.w,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getReportPageUnselectedBorderColor(), width: 2.w), border: Border.all(
color:
SCGlobalConfig.businessLogicStrategy
.getReportPageUnselectedBorderColor(),
width: 2.w,
),
), ),
), ),
], ],

View File

@ -78,6 +78,13 @@ class _RoomEditPageState extends State<RoomEditPage> {
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""; rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomCover ?? "";
} }
@override
void dispose() {
_roomNameController.dispose();
_roomAnnouncementController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return WillPopScope(

View File

@ -39,6 +39,12 @@ class _RoomGiftRankPageState extends State<RoomGiftRankPage>
_tabController = TabController(length: _pages.length, vsync: this); _tabController = TabController(length: _pages.length, vsync: this);
} }
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_tabs.clear(); _tabs.clear();
@ -97,5 +103,4 @@ class _RoomGiftRankPageState extends State<RoomGiftRankPage>
), ),
); );
} }
} }

View File

@ -33,6 +33,8 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
RtcProvider? provider; RtcProvider? provider;
JoinRoomRes? room; JoinRoomRes? room;
MicRes? roomSeat; MicRes? roomSeat;
JoinRoomRes? _cachedRoom;
MicRes? _cachedRoomSeat;
final GlobalKey _targetKey = GlobalKey(); final GlobalKey _targetKey = GlobalKey();
@override @override
@ -44,8 +46,22 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
roomSeat = provider?.roomWheatMap[widget.index]; final liveRoomSeat = provider?.roomWheatMap[widget.index];
room = provider?.currenRoom; 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( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: Column( child: Column(
@ -78,7 +94,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
? "" ? ""
: roomSeat?.user?.getHeaddress()?.sourceUrl, : roomSeat?.user?.getHeaddress()?.sourceUrl,
) )
: (roomSeat!.micLock! : ((roomSeat?.micLock ?? false)
? Image.asset( ? Image.asset(
"sc_images/room/sc_icon_seat_lock.png", "sc_images/room/sc_icon_seat_lock.png",
width: widget.isGameModel ? 38.w : 52.w, width: widget.isGameModel ? 38.w : 52.w,
@ -93,7 +109,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
bottom: widget.isGameModel ? 2.w : 5.w, bottom: widget.isGameModel ? 2.w : 5.w,
right: widget.isGameModel ? 2.w : 5.w, right: widget.isGameModel ? 2.w : 5.w,
child: child:
roomSeat!.micMute! (roomSeat?.micMute ?? false)
? Image.asset( ? Image.asset(
"sc_images/room/sc_icon_room_seat_mic_mute.png", "sc_images/room/sc_icon_room_seat_mic_mute.png",
width: 14.w, width: 14.w,

View File

@ -2,7 +2,6 @@ import 'dart:math' as math;
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.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/app/constants/sc_global_config.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
@ -53,7 +52,6 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
late TabController _tabController; late TabController _tabController;
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()]; final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
final List<Widget> _tabs = [];
late StreamSubscription _subscription; late StreamSubscription _subscription;
final RoomGiftSeatFlightController _giftSeatFlightController = final RoomGiftSeatFlightController _giftSeatFlightController =
RoomGiftSeatFlightController(); RoomGiftSeatFlightController();
@ -65,7 +63,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
super.initState(); super.initState();
_tabController = TabController(length: _pages.length, vsync: this); _tabController = TabController(length: _pages.length, vsync: this);
_enableRoomVisualEffects(); _enableRoomVisualEffects();
_tabController.addListener(() {}); // _tabController.addListener(_handleTabChange);
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen(( _subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
event, event,
@ -91,11 +89,19 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
@override @override
void dispose() { void dispose() {
_suspendRoomVisualEffects(); _suspendRoomVisualEffects();
_tabController.removeListener(_handleTabChange);
_tabController.dispose(); // _tabController.dispose(); //
_subscription.cancel(); _subscription.cancel();
super.dispose(); super.dispose();
} }
void _handleTabChange() {
if (!mounted) {
return;
}
setState(() {});
}
void _enableRoomVisualEffects() { void _enableRoomVisualEffects() {
Provider.of<RtcProvider>( Provider.of<RtcProvider>(
context, context,
@ -150,10 +156,6 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
@override @override
Widget build(BuildContext context) { 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( return PopScope(
canPop: false, canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) { onPopInvokedWithResult: (bool didPop, Object? result) {
@ -236,20 +238,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
children: [ children: [
TabBar( TabBar(
tabAlignment: TabAlignment.start, tabAlignment: TabAlignment.start,
labelPadding: SCGlobalConfig.businessLogicStrategy splashFactory: NoSplash.splashFactory,
.getVoiceRoomTabLabelPadding() overlayColor: WidgetStateProperty.all(Colors.transparent),
.copyWith( labelPadding: EdgeInsets.symmetric(horizontal: 8.w),
left:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabLabelPadding()
.left *
ScreenUtil().setWidth(1),
right:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabLabelPadding()
.right *
ScreenUtil().setWidth(1),
),
labelColor: labelColor:
SCGlobalConfig.businessLogicStrategy SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabLabelColor(), .getVoiceRoomTabLabelColor(),
@ -283,7 +274,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
SCGlobalConfig.businessLogicStrategy SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabDividerColor(), .getVoiceRoomTabDividerColor(),
controller: _tabController, controller: _tabController,
tabs: _tabs, tabs: List<Widget>.generate(_pages.length, _buildImageTab),
), ),
Expanded( Expanded(
child: Container( child: Container(
@ -314,6 +305,38 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
); );
} }
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) { _floatingGiftListener(Msg msg) {
if (!Provider.of<RtcProvider>( if (!Provider.of<RtcProvider>(

View File

@ -548,7 +548,7 @@ class _SearchRoomListState extends State<SearchRoomList> {
: Container(height: 10.w), : Container(height: 10.w),
(e.extValues?.roomSetting?.password?.isEmpty ?? false) (e.extValues?.roomSetting?.password?.isEmpty ?? false)
? text( ? text(
e.extValues?.memberQuantity ?? "0", e.displayMemberCount,
fontSize: 10.sp, fontSize: 10.sp,
lineHeight: 1, lineHeight: 1,
) )

View File

@ -29,12 +29,18 @@ class _LevelPageState extends State<LevelPage>
}); });
} }
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
children: [ children: [
Image.asset( Image.asset(
"sc_images/person/sc_icon_edit_userinfo_bg.png", "sc_images/person/sc_icon_edit_userinfo_bg.png",
width: ScreenUtil().screenWidth, width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight, height: ScreenUtil().screenHeight,
fit: BoxFit.fill, fit: BoxFit.fill,

View File

@ -7,8 +7,6 @@ import 'package:yumi/modules/user/my_items/theme/bags_theme_page.dart';
///- ///-
class BagsTabThemePage extends StatefulWidget { class BagsTabThemePage extends StatefulWidget {
@override @override
_BagsTabThemePageState createState() => _BagsTabThemePageState(); _BagsTabThemePageState createState() => _BagsTabThemePageState();
} }
@ -22,14 +20,18 @@ class _BagsTabThemePageState extends State<BagsTabThemePage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_pages.add( _pages.add(BagsThemePage());
BagsThemePage(),
);
_pages.add(RoomThemeCustomPage()); _pages.add(RoomThemeCustomPage());
_tabController = TabController(length: _pages.length, vsync: this); _tabController = TabController(length: _pages.length, vsync: this);
_tabController.addListener(() {}); // _tabController.addListener(() {}); //
} }
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_tabs.clear(); _tabs.clear();

View File

@ -160,6 +160,13 @@ class _PersonDetailPageState extends State<PersonDetailPage>
return Tab(text: text); return Tab(text: text);
} }
@override
void dispose() {
_tabController.dispose();
_scrollController.dispose();
super.dispose();
}
Widget _buildProfileHeader( Widget _buildProfileHeader(
SocialChatUserProfileManager ref, SocialChatUserProfileManager ref,
List<PersonPhoto> backgroundPhotos, List<PersonPhoto> backgroundPhotos,

View File

@ -33,6 +33,14 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
/// ///
TextEditingController oldPassController = TextEditingController(); TextEditingController oldPassController = TextEditingController();
@override
void dispose() {
confirmController.dispose();
passController.dispose();
oldPassController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
@ -50,289 +58,308 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
title: SCAppLocalizations.of(context)!.resetLoginPassword, title: SCAppLocalizations.of(context)!.resetLoginPassword,
actions: [], actions: [],
), ),
body: SafeArea(top: false,child: Column( body: SafeArea(
children: [ top: false,
Container( child: Column(
width: ScreenUtil().screenWidth, children: [
padding: EdgeInsets.symmetric(vertical: 8.w, horizontal: 8.w), Container(
child: text( width: ScreenUtil().screenWidth,
SCAppLocalizations.of(context)!.resetLoginPasswordtTips1, padding: EdgeInsets.symmetric(vertical: 8.w, horizontal: 8.w),
maxLines: 2, child: text(
textColor: Colors.white, SCAppLocalizations.of(context)!.resetLoginPasswordtTips1,
fontSize: 10.sp, maxLines: 2,
),
),
SizedBox(height: 10.w),
Row(
children: [
SizedBox(width: 15.w),
text(
SCAppLocalizations.of(context)!.account,
fontSize: 15.sp,
textColor: Colors.white, 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( SizedBox(height: 10.w),
AccountStorage().getCurrentUser()?.userProfile?.account ?? "", Row(
textColor: Colors.white54, children: [
fontSize: 14.sp, SizedBox(width: 15.w),
), text(
), SCAppLocalizations.of(context)!.account,
SizedBox(height: 10.w), fontSize: 15.sp,
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, textColor: Colors.white,
maxLines: 3,
), ),
],
),
Container(
width: ScreenUtil().screenWidth,
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 10.w,
), ),
SizedBox(width: 10.w), margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w),
],
),
SizedBox(height: 20.w),
GestureDetector(
child: Container(
height: 44.w,
width: 260.w,
alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: SocialChatTheme.primaryLight, color: Color(0xff18F2B1).withOpacity(0.1),
), ),
child: text( child: text(
SCAppLocalizations.of(context)!.submit, AccountStorage().getCurrentUser()?.userProfile?.account ??
fontSize: 16.sp, "",
textColor: Colors.white, textColor: Colors.white54,
fontSize: 14.sp,
), ),
), ),
onTap: () { SizedBox(height: 10.w),
resetPwd(); 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();
},
),
],
),
),
), ),
], ],
); );

View File

@ -29,6 +29,13 @@ class _SetPwdPageState extends State<SetPwdPage> {
/// ///
TextEditingController passController = TextEditingController(); TextEditingController passController = TextEditingController();
@override
void dispose() {
confirmController.dispose();
passController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(

View File

@ -45,6 +45,12 @@ class _UserBlockedListPageState
loadData(1); loadData(1);
} }
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
@ -118,13 +124,15 @@ class _UserBlockedListPageState
child: Row( child: Row(
children: [ children: [
SizedBox(width: 10.w), SizedBox(width: 10.w),
SCDebounceWidget(child: head(url: res.userProfile?.userAvatar ?? "", width: 62.w), onTap: (){ SCDebounceWidget(
SCNavigatorUtils.push( child: head(url: res.userProfile?.userAvatar ?? "", width: 62.w),
context, onTap: () {
"${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.userProfile?.id}&tageId=${res.userProfile?.id}", SCNavigatorUtils.push(
); context,
}) "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.userProfile?.id}&tageId=${res.userProfile?.id}",
, );
},
),
SizedBox(width: 10.w), SizedBox(width: 10.w),
Expanded( Expanded(
child: Column( child: Column(
@ -134,11 +142,14 @@ class _UserBlockedListPageState
children: [ children: [
netImage( netImage(
url: url:
Provider.of<SCAppGeneralManager>(context, listen: false) Provider.of<SCAppGeneralManager>(
.findCountryByName( context,
res.userProfile?.countryName ?? "", listen: false,
) )
?.nationalFlag ?? .findCountryByName(
res.userProfile?.countryName ?? "",
)
?.nationalFlag ??
"", "",
borderRadius: BorderRadius.all(Radius.circular(3.w)), borderRadius: BorderRadius.all(Radius.circular(3.w)),
width: 19.w, width: 19.w,
@ -150,14 +161,15 @@ class _UserBlockedListPageState
textColor: Colors.white, textColor: Colors.white,
res.userProfile?.userNickname ?? "", res.userProfile?.userNickname ?? "",
fontSize: 14.sp, fontSize: 14.sp,
type:res.userProfile?.getVIP()?.name ?? "", type: res.userProfile?.getVIP()?.name ?? "",
needScroll: needScroll:
(res.userProfile?.userNickname?.characters.length ?? 0) > (res.userProfile?.userNickname?.characters.length ??
0) >
10, 10,
), ),
SizedBox(width: 3.w), SizedBox(width: 3.w),
Container( Container(
width: (res.userProfile?.age??0)>999?58.w:48.w, width: (res.userProfile?.age ?? 0) > 999 ? 58.w : 48.w,
height: 24.w, height: 24.w,
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
@ -234,13 +246,15 @@ class _UserBlockedListPageState
], ],
), ),
), ),
SCDebounceWidget(child: Image.asset( SCDebounceWidget(
"sc_images/room/sc_icon_block_list_delete.png", child: Image.asset(
height: 20.w, "sc_images/room/sc_icon_block_list_delete.png",
), onTap: (){ height: 20.w,
_delete(res.userProfile); ),
}) onTap: () {
, _delete(res.userProfile);
},
),
SizedBox(width: 15.w), SizedBox(width: 15.w),
], ],
), ),
@ -257,7 +271,7 @@ class _UserBlockedListPageState
setState(() {}); setState(() {});
}) })
.catchError((e) { .catchError((e) {
SCLoadingManager.hide(); SCLoadingManager.hide();
loadComplete(); loadComplete();
setState(() {}); setState(() {});
}); });
@ -307,46 +321,29 @@ class _UserBlockedListPageState
void _delete(SocialChatUserProfile? userProfile) { void _delete(SocialChatUserProfile? userProfile) {
SmartDialog.show( SmartDialog.show(
tag: "showConfirmDialog", tag: "showConfirmDialog",
alignment: alignment: Alignment.center,
Alignment.center,
debounce: true, debounce: true,
animationType: animationType: SmartAnimationType.fade,
SmartAnimationType
.fade,
builder: (_) { builder: (_) {
return MsgDialog( return MsgDialog(
title: title: SCAppLocalizations.of(context!)!.tips,
SCAppLocalizations.of( msg: SCAppLocalizations.of(context)!.areYouSureToCancelBlacklist,
context!, btnText: SCAppLocalizations.of(context)!.confirm,
)!.tips,
msg:SCAppLocalizations.of(
context,
)!.areYouSureToCancelBlacklist,
btnText:
SCAppLocalizations.of(
context,
)!.confirm,
onEnsure: () { onEnsure: () {
SCLoadingManager.show(); SCLoadingManager.show();
SCAccountRepository() SCAccountRepository()
.deleteUserBlacklist( .deleteUserBlacklist(userProfile?.id ?? "")
userProfile?.id??"", .then((result) {
) SCTts.show(
.then(( SCAppLocalizations.of(
result, context,
) { )!.successfullyRemovedFromTheBlacklist,
SCTts.show( );
SCAppLocalizations.of( loadData(1);
context, })
)!.successfullyRemovedFromTheBlacklist, .catchError((e) {
); SCLoadingManager.hide();
loadData(1); });
})
.catchError((
e,
) {
SCLoadingManager.hide();
});
}, },
); );
}, },

View File

@ -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<MiFaPayWebViewPage> createState() => _MiFaPayWebViewPageState();
}
class _MiFaPayWebViewPageState extends State<MiFaPayWebViewPage> {
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 <Widget>[],
backgroundColor: Colors.white,
backButtonColor: Colors.black,
gradient: const LinearGradient(
colors: <Color>[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),
),
),
],
),
);
}
}

View File

@ -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<RechargePackageOption> packages;
final List<RechargeChannelOption> channels;
final void Function(
RechargePackageOption package,
RechargeChannelOption channel,
)
onConfirm;
final String? initialPackageId;
final String? initialChannelCode;
@override
State<RechargeMethodBottomSheet> createState() =>
_RechargeMethodBottomSheetState();
}
class _RechargeMethodBottomSheetState extends State<RechargeMethodBottomSheet> {
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();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:fluro/fluro.dart'; import 'package:fluro/fluro.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -44,8 +46,16 @@ typedef OnSoundVoiceChange = Function(num index, int volum);
typedef RtcProvider = RealTimeCommunicationManager; typedef RtcProvider = RealTimeCommunicationManager;
class RealTimeCommunicationManager extends ChangeNotifier { class RealTimeCommunicationManager extends ChangeNotifier {
static const Duration _micListPollingInterval = Duration(seconds: 2);
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
bool needUpDataUserInfo = false; bool needUpDataUserInfo = false;
bool _roomVisualEffectsEnabled = false; bool _roomVisualEffectsEnabled = false;
bool _isExitingCurrentVoiceRoomSession = false;
Timer? _micListPollingTimer;
Timer? _onlineUsersPollingTimer;
bool _isRefreshingMicList = false;
bool _isRefreshingOnlineUsers = false;
/// ///
JoinRoomRes? currenRoom; JoinRoomRes? currenRoom;
@ -91,6 +101,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled; bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled;
bool get isExitingCurrentVoiceRoomSession =>
_isExitingCurrentVoiceRoomSession;
bool get shouldShowRoomVisualEffects => bool get shouldShowRoomVisualEffects =>
currenRoom != null && _roomVisualEffectsEnabled; currenRoom != null && _roomVisualEffectsEnabled;
@ -102,6 +115,186 @@ class RealTimeCommunicationManager extends ChangeNotifier {
notifyListeners(); 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<SocialChatUserProfile> previous,
List<SocialChatUserProfile> 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<num, MicRes> previous, Map<num, MicRes> 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<SocialChatUserProfile> users) {
managerUsers
..clear()
..addAll(
users.where((user) => user.roles == SCRoomRolesType.ADMIN.name),
);
}
Map<num, MicRes> _buildMicMap(List<MicRes> roomWheatList) {
final previousMap = roomWheatMap;
final nextMap = <num, MicRes>{};
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<num, MicRes> nextMap, {
bool notifyIfUnchanged = true,
}) {
final changed = !_sameMicMaps(roomWheatMap, nextMap);
roomWheatMap = nextMap;
_syncSelfMicRuntimeState();
if (changed || notifyIfUnchanged) {
notifyListeners();
}
}
Future<void> joinAgoraVoiceChannel() async { Future<void> joinAgoraVoiceChannel() async {
try { try {
engine = await _initAgoraRtcEngine(); engine = await _initAgoraRtcEngine();
@ -343,6 +536,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
listen: false, listen: false,
).fetchUserProfileData(); ).fetchUserProfileData();
retrieveMicrophoneList(); retrieveMicrophoneList();
fetchOnlineUsersList();
_startRoomStatePolling();
setRoomVisualEffectsEnabled(true); setRoomVisualEffectsEnabled(true);
SCFloatIchart().remove(); SCFloatIchart().remove();
SCNavigatorUtils.push( SCNavigatorUtils.push(
@ -453,6 +648,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
/// ///
retrieveMicrophoneList(); retrieveMicrophoneList();
fetchOnlineUsersList();
_startRoomStatePolling();
Provider.of<SocialChatRoomManager>( Provider.of<SocialChatRoomManager>(
context!, context!,
listen: false, listen: false,
@ -510,46 +707,58 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
///线 ///线
Future fetchOnlineUsersList() async { Future<void> fetchOnlineUsersList({bool notifyIfUnchanged = true}) async {
onlineUsers = await SCChatRoomRepository().roomOnlineUsers( final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
currenRoom?.roomProfile?.roomProfile?.id ?? "", if (roomId.isEmpty || _isRefreshingOnlineUsers) {
); return;
managerUsers.clear(); }
for (var user in onlineUsers) { _isRefreshingOnlineUsers = true;
if (user.roles == SCRoomRolesType.ADMIN.name) { try {
managerUsers.add(user); 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;
} }
notifyListeners();
} }
Future retrieveMicrophoneList() async { Future<void> retrieveMicrophoneList({bool notifyIfUnchanged = true}) async {
bool isOnMic = false; final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
var roomWheatList = await SCChatRoomRepository().micList( if (roomId.isEmpty || _isRefreshingMicList) {
currenRoom!.roomProfile?.roomProfile?.id ?? "", return;
); }
for (var roomWheat in roomWheatList) { _isRefreshingMicList = true;
roomWheatMap[roomWheat.micIndex!] = roomWheat; bool isOnMic = false;
if (roomWheat.user != null && try {
roomWheat.user!.id == final roomWheatList = await SCChatRoomRepository().micList(roomId);
AccountStorage().getCurrentUser()?.userProfile?.id) { if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
isOnMic = true; return;
}
/// final nextMap = _buildMicMap(roomWheatList);
SCHeartbeatUtils.scheduleAnchorHeartbeat( for (final roomWheat in nextMap.values) {
currenRoom?.roomProfile?.roomProfile?.id ?? "", 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;
} }
notifyListeners();
SCHeartbeatUtils.scheduleHeartbeat(
SCHeartbeatStatus.VOICE_LIVE.name,
isOnMic,
roomId: currenRoom?.roomProfile?.roomProfile?.id,
);
Future.delayed(Duration(milliseconds: 1500), () {
fetchOnlineUsersList();
});
} }
void fetchRoomTaskClaimableCount() { void fetchRoomTaskClaimableCount() {
@ -577,6 +786,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
Future exitCurrentVoiceRoomSession(bool isLogout) async { Future exitCurrentVoiceRoomSession(bool isLogout) async {
_setExitingCurrentVoiceRoomSession(true);
_stopRoomStatePolling();
try { try {
rtmProvider?.msgAllListener = null; rtmProvider?.msgAllListener = null;
rtmProvider?.msgChatListener = null; rtmProvider?.msgChatListener = null;
@ -613,6 +824,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Future<void> resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async { Future<void> resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async {
rtmProvider ??= fallbackRtmProvider; rtmProvider ??= fallbackRtmProvider;
_stopRoomStatePolling();
try { try {
SCHeartbeatUtils.cancelTimer(); SCHeartbeatUtils.cancelTimer();
SCHeartbeatUtils.cancelAnchorTimer(); SCHeartbeatUtils.cancelAnchorTimer();
@ -635,12 +847,14 @@ class RealTimeCommunicationManager extends ChangeNotifier {
/// ///
void _clearData() { void _clearData() {
_stopRoomStatePolling();
roomRocketStatus = null; roomRocketStatus = null;
rtmProvider rtmProvider
?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] =
null; null;
roomWheatMap.clear(); roomWheatMap.clear();
onlineUsers.clear(); onlineUsers.clear();
managerUsers.clear();
needUpDataUserInfo = false; needUpDataUserInfo = false;
SCRoomUtils.roomUsersMap.clear(); SCRoomUtils.roomUsersMap.clear();
roomIsMute = false; roomIsMute = false;
@ -649,6 +863,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
redPacketList.clear(); redPacketList.clear();
currenRoom = null; currenRoom = null;
_roomVisualEffectsEnabled = false; _roomVisualEffectsEnabled = false;
_isExitingCurrentVoiceRoomSession = false;
isMic = true; isMic = true;
isMusicPlaying = false; isMusicPlaying = false;
DataPersistence.setLastTimeRoomId(""); DataPersistence.setLastTimeRoomId("");
@ -745,6 +960,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
if (seatUser.id == currentUserId) { if (seatUser.id == currentUserId) {
if (isRoomAdmin) {
return false;
}
_openRoomUserInfoCard(seatUser.id); _openRoomUserInfoCard(seatUser.id);
return true; return true;
} }
@ -757,6 +975,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return false; return false;
} }
void _refreshMicListSilently() {
retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {});
}
void _openRoomUserInfoCard(String? userId) { void _openRoomUserInfoCard(String? userId) {
final normalizedUserId = (userId ?? '').trim(); final normalizedUserId = (userId ?? '').trim();
if (normalizedUserId.isEmpty) { if (normalizedUserId.isEmpty) {
@ -813,48 +1035,54 @@ class RealTimeCommunicationManager extends ChangeNotifier {
SCHeartbeatUtils.scheduleAnchorHeartbeat( SCHeartbeatUtils.scheduleAnchorHeartbeat(
currenRoom?.roomProfile?.roomProfile?.id ?? "", currenRoom?.roomProfile?.roomProfile?.id ?? "",
); );
var us = roomWheatMap[index]; final currentSeat = roomWheatMap[index];
us?.copyWith( if (currentSeat != null && myUser != null) {
user: SocialChatUserProfile( roomWheatMap[index] = currentSeat.copyWith(
id: myUser?.id, user: myUser.copyWith(roles: currenRoom?.entrants?.roles),
account: myUser?.account, micMute: micGoUpRes.micMute,
userAvatar: myUser?.userAvatar, roomToken: micGoUpRes.roomToken,
userNickname: myUser?.userNickname, );
userSex: myUser?.userSex, }
), if (roomWheatMap[index]?.micMute ?? false) {
);
roomWheatMap[index] = us!;
if (us.micMute!) {
/// ///
if (isFz()) { if (isFz()) {
jieJinMai(index); await jieJinMai(index);
} }
} }
notifyListeners(); notifyListeners();
_refreshMicListSilently();
} catch (ex) { } catch (ex) {
SCTts.show('Failed to put on the microphone, $ex'); SCTts.show('Failed to put on the microphone, $ex');
} }
} }
xiaMai(num index) async { xiaMai(num index) async {
SCChatRoomRepository() try {
.micGoDown(currenRoom?.roomProfile?.roomProfile?.id ?? "", index) await SCChatRoomRepository().micGoDown(
.whenComplete(() { currenRoom?.roomProfile?.roomProfile?.id ?? "",
// index,
if (roomWheatMap[index]?.user?.id == );
AccountStorage().getCurrentUser()?.userProfile?.id) {
isMic = true;
engine?.muteLocalAudioStream(true);
}
SCHeartbeatUtils.cancelAnchorTimer();
/// if (roomWheatMap[index]?.user?.id ==
engine?.renewToken(""); AccountStorage().getCurrentUser()?.userProfile?.id) {
engine?.setClientRole(role: ClientRoleType.clientRoleAudience); isMic = true;
roomWheatMap[index]?.setUser = null; engine?.muteLocalAudioStream(true);
notifyListeners(); }
}); 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');
}
} }
/// ///
@ -953,32 +1181,15 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (mics == null || mics.isEmpty) { if (mics == null || mics.isEmpty) {
return; return;
} }
roomWheatMap.clear(); _applyMicSnapshot(_buildMicMap(mics));
for (var mic in mics) { final isOnMic =
roomWheatMap[mic.micIndex!] = mic; userOnMaiInIndex(AccountStorage().getCurrentUser()?.userProfile?.id ?? "") >
if (mic.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { -1;
if (mic.micMute!) { SCHeartbeatUtils.scheduleHeartbeat(
/// SCHeartbeatStatus.VOICE_LIVE.name,
engine?.muteLocalAudioStream(true); isOnMic,
} else { roomId: currenRoom?.roomProfile?.roomProfile?.id,
// 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);
}
} }
/// -1 /// -1
@ -1044,25 +1255,37 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
/// ///
void jieFeng(num index) async { Future<void> jieFeng(num index) async {
await SCChatRoomRepository().micLock( await SCChatRoomRepository().micLock(
currenRoom?.roomProfile?.roomProfile?.id ?? "", currenRoom?.roomProfile?.roomProfile?.id ?? "",
index, index,
false, false,
); );
final mic = roomWheatMap[index];
if (mic != null) {
roomWheatMap[index] = mic.copyWith(micLock: false);
notifyListeners();
}
_refreshMicListSilently();
} }
/// ///
void fengMai(num index) async { Future<void> fengMai(num index) async {
await SCChatRoomRepository().micLock( await SCChatRoomRepository().micLock(
currenRoom?.roomProfile?.roomProfile?.id ?? "", currenRoom?.roomProfile?.roomProfile?.id ?? "",
index, index,
true, true,
); );
final mic = roomWheatMap[index];
if (mic != null) {
roomWheatMap[index] = mic.copyWith(micLock: true);
notifyListeners();
}
_refreshMicListSilently();
} }
/// ///
void jinMai(num index) async { Future<void> jinMai(num index) async {
await SCChatRoomRepository().micMute( await SCChatRoomRepository().micMute(
currenRoom?.roomProfile?.roomProfile?.id ?? "", currenRoom?.roomProfile?.roomProfile?.id ?? "",
index, index,
@ -1073,16 +1296,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
context!, context!,
listen: false, listen: false,
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
Provider.of<RtcProvider>(
context!,
listen: false,
).engine?.muteLocalAudioStream(true);
} }
var mic = roomWheatMap[index]; var mic = roomWheatMap[index];
if (mic != null) { if (mic != null) {
roomWheatMap[index] = mic.copyWith(micMute: true); roomWheatMap[index] = mic.copyWith(micMute: true);
notifyListeners(); notifyListeners();
} }
_refreshMicListSilently();
} }
/// ///
void jieJinMai(num index) async { Future<void> jieJinMai(num index) async {
await SCChatRoomRepository().micMute( await SCChatRoomRepository().micMute(
currenRoom?.roomProfile?.roomProfile?.id ?? "", currenRoom?.roomProfile?.roomProfile?.id ?? "",
index, index,
@ -1109,6 +1337,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
roomWheatMap[index] = mic.copyWith(micMute: false); roomWheatMap[index] = mic.copyWith(micMute: false);
notifyListeners(); notifyListeners();
} }
_refreshMicListSilently();
} }
void addOnlineUser(String groupId, SocialChatUserProfile user) { void addOnlineUser(String groupId, SocialChatUserProfile user) {
@ -1124,6 +1353,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
if (!isExtOnlineList) { if (!isExtOnlineList) {
Provider.of<RtcProvider>(context!, listen: false).onlineUsers.add(user); Provider.of<RtcProvider>(context!, listen: false).onlineUsers.add(user);
_refreshManagerUsers(onlineUsers);
notifyListeners(); notifyListeners();
} }
} }
@ -1144,6 +1374,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
context!, context!,
listen: false, listen: false,
).onlineUsers.remove(isExtOnlineUser); ).onlineUsers.remove(isExtOnlineUser);
_refreshManagerUsers(onlineUsers);
notifyListeners(); notifyListeners();
} }
} }

View File

@ -71,6 +71,7 @@ typedef RtmProvider = RealTimeMessagingManager;
class RealTimeMessagingManager extends ChangeNotifier { class RealTimeMessagingManager extends ChangeNotifier {
static const int _giftComboMergeWindowMs = 3000; static const int _giftComboMergeWindowMs = 3000;
static const int _maxLuckGiftPushQueueLength = 12;
BuildContext? context; BuildContext? context;
@ -257,13 +258,26 @@ class RealTimeMessagingManager extends ChangeNotifier {
); );
TencentImSDKPlugin.v2TIMManager.addGroupListener( TencentImSDKPlugin.v2TIMManager.addGroupListener(
listener: V2TimGroupListener( listener: V2TimGroupListener(
onMemberEnter: onMemberEnter: (String groupID, List<V2TimGroupMemberInfo> memberList) {
(String groupID, List<V2TimGroupMemberInfo> memberList) {}, final rtcProvider = Provider.of<RealTimeCommunicationManager>(
onMemberLeave: (String groupID, V2TimGroupMemberInfo member) {
Provider.of<RealTimeCommunicationManager>(
context, context,
listen: false, 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<RealTimeCommunicationManager>(
context,
listen: false,
);
rtcProvider.removOnlineUser(groupID, member.userID!);
if (groupID ==
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) {
rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false);
}
}, },
onMemberKicked: ( onMemberKicked: (
String groupID, String groupID,
@ -275,13 +289,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
if (memberList.first.userID == if (memberList.first.userID ==
AccountStorage().getCurrentUser()?.userProfile?.id) { AccountStorage().getCurrentUser()?.userProfile?.id) {
Provider.of<RealTimeCommunicationManager>( Provider.of<RealTimeCommunicationManager>(
context!, context,
listen: false, listen: false,
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
///退 ///退
Provider.of<RealTimeCommunicationManager>( Provider.of<RealTimeCommunicationManager>(
context!, context,
listen: false, listen: false,
).exitCurrentVoiceRoomSession(false).whenComplete(() { ).exitCurrentVoiceRoomSession(false).whenComplete(() {
SCRoomUtils.closeAllDialogs(); SCRoomUtils.closeAllDialogs();
@ -1314,7 +1328,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
listen: false, listen: false,
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
} }
// Provider.of<RealTimeCommunicationManager>(context!, listen: false).getMicList(); Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).retrieveMicrophoneList(notifyIfUnchanged: false);
return; return;
} }
@ -1353,11 +1370,19 @@ class RealTimeMessagingManager extends ChangeNotifier {
context!, context!,
listen: false, listen: false,
).micChange(BroadCastMicChangePush.fromJson(data).data?.mics); ).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<RealTimeCommunicationManager>(
context!,
listen: false,
).retrieveMicrophoneList(notifyIfUnchanged: false);
} else if (msg.type == SCRoomMsgType.refreshOnlineUser) { } else if (msg.type == SCRoomMsgType.refreshOnlineUser) {
Provider.of<RealTimeCommunicationManager>( Provider.of<RealTimeCommunicationManager>(
context!, context!,
listen: false, listen: false,
).fetchOnlineUsersList(); ).fetchOnlineUsersList(notifyIfUnchanged: false);
} else if (msg.type == SCRoomMsgType.gameLuckyGift) { } else if (msg.type == SCRoomMsgType.gameLuckyGift) {
var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data);
_giftFxLog( _giftFxLog(
@ -1669,6 +1694,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) {
if (SCGlobalConfig.isLuckGiftSpecialEffects) { if (SCGlobalConfig.isLuckGiftSpecialEffects) {
while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) {
_luckGiftPushQueue.removeFirst();
}
_luckGiftPushQueue.add(broadCastRes); _luckGiftPushQueue.add(broadCastRes);
playLuckGiftBackCoins(); playLuckGiftBackCoins();
} }

View File

@ -5,6 +5,8 @@ import 'package:flutter/cupertino.dart';
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
class GiftAnimationManager extends ChangeNotifier { class GiftAnimationManager extends ChangeNotifier {
static const int _maxPendingAnimations = 24;
Queue<LGiftModel> pendingAnimationsQueue = Queue<LGiftModel>(); Queue<LGiftModel> pendingAnimationsQueue = Queue<LGiftModel>();
List<LGiftScrollingScreenAnimsBean> animationControllerList = []; List<LGiftScrollingScreenAnimsBean> animationControllerList = [];
@ -15,10 +17,77 @@ class GiftAnimationManager extends ChangeNotifier {
animationControllerList.length >= giftMap.length; animationControllerList.length >= giftMap.length;
void enqueueGiftAnimation(LGiftModel giftModel) { void enqueueGiftAnimation(LGiftModel giftModel) {
if (_mergeIntoActiveAnimation(giftModel)) {
return;
}
if (_mergeIntoPendingAnimation(giftModel)) {
return;
}
_trimPendingAnimations();
pendingAnimationsQueue.add(giftModel); pendingAnimationsQueue.add(giftModel);
proceedToNextAnimation(); 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() { proceedToNextAnimation() {
if (pendingAnimationsQueue.isEmpty || !_controllersReady) { if (pendingAnimationsQueue.isEmpty || !_controllersReady) {
@ -35,28 +104,7 @@ class GiftAnimationManager extends ChangeNotifier {
break; break;
} else { } else {
if (value.labelId == playGift.labelId) { if (value.labelId == playGift.labelId) {
playGift.giftCount = value.giftCount + playGift.giftCount; _mergeGiftModel(target: value, incoming: playGift);
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;
pendingAnimationsQueue.removeFirst(); pendingAnimationsQueue.removeFirst();
notifyListeners(); notifyListeners();
animationControllerList[key].controller.forward(from: 0.45); animationControllerList[key].controller.forward(from: 0.45);
@ -80,6 +128,7 @@ class GiftAnimationManager extends ChangeNotifier {
for (var element in animationControllerList) { for (var element in animationControllerList) {
element.controller.dispose(); element.controller.dispose();
} }
animationControllerList.clear();
} }
void markAnimationAsFinished(int index) { void markAnimationAsFinished(int index) {

View File

@ -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<SCMiFaPayCountryRes> _countries = <SCMiFaPayCountryRes>[];
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<SCMiFaPayCountryRes> get countries => _countries;
SCMiFaPayCountryRes? get selectedCountry => _selectedCountry;
SCMiFaPayCommodityRes? get commodityRes => _commodityRes;
List<SCMiFaPayCommodityItemRes> get commodities =>
_commodityRes?.commodity ?? <SCMiFaPayCommodityItemRes>[];
List<SCMiFaPayChannelRes> get channels =>
_commodityRes?.channels ?? <SCMiFaPayChannelRes>[];
SCMiFaPayCommodityItemRes? get selectedCommodity => _selectedCommodity;
SCMiFaPayChannelRes? get selectedChannel => _selectedChannel;
bool get canCreateRecharge =>
_selectedCountry != null &&
_selectedCommodity != null &&
_selectedChannel != null &&
!_isLoading &&
!_isCreatingOrder;
Future<void> 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<void> 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<void> 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<String, dynamic> payload = <String, dynamic>{
"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<SocialChatUserProfileManager>(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<void>(
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<void> _loadCountries() async {
List<SCMiFaPayCountryRes> countries = <SCMiFaPayCountryRes>[];
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 = <SCMiFaPayCountryRes>[_fallbackCountry()];
}
_countries = countries;
final String currentCountryId = _selectedCountry?.id ?? '';
_selectedCountry = countries.firstWhere(
(SCMiFaPayCountryRes item) => item.id == currentCountryId,
orElse: () => countries.first,
);
}
Future<void> _loadCommodity() async {
final String payCountryId =
_selectedCountry?.id?.isNotEmpty == true
? _selectedCountry!.id!
: defaultPayCountryId;
final Map<String, dynamic> queryParams = <String, dynamic>{
"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<SCMiFaPayCommodityItemRes> commodityList = commodities;
final String currentGoodsId = _selectedCommodity?.id ?? '';
_selectedCommodity =
commodityList.isEmpty
? null
: commodityList.firstWhere(
(SCMiFaPayCommodityItemRes item) => item.id == currentGoodsId,
orElse: () => commodityList.first,
);
final List<SCMiFaPayChannelRes> 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<void> _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<void> _pollWalletState(
SocialChatUserProfileManager profileManager,
) async {
for (int index = 0; index < 2; index++) {
await Future<void>.delayed(const Duration(seconds: 3));
await _refreshWalletState(profileManager);
}
}
Future<bool> _waitForRechargeResult(
SocialChatUserProfileManager profileManager,
double previousBalance,
) async {
await _refreshWalletState(profileManager);
if (profileManager.myBalance > previousBalance) {
return true;
}
for (int index = 0; index < 5; index++) {
await Future<void>.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<String, dynamic>? queryParams,
Map<String, dynamic>? 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<String, dynamic>? queryParams,
Map<String, dynamic>? 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();
}
}
}

View File

@ -72,6 +72,7 @@ class RoomProfile {
String? userId, String? userId,
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
String? userSVipLevel, String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues, ExtValues? extValues,
}) { }) {
_countryCode = countryCode; _countryCode = countryCode;
@ -91,6 +92,7 @@ class RoomProfile {
_userId = userId; _userId = userId;
_userProfile = userProfile; _userProfile = userProfile;
_userSVipLevel = userSVipLevel; _userSVipLevel = userSVipLevel;
_roomCounter = roomCounter;
_extValues = extValues; _extValues = extValues;
} }
@ -118,6 +120,9 @@ class RoomProfile {
? SocialChatUserProfile.fromJson(json['userProfile']) ? SocialChatUserProfile.fromJson(json['userProfile'])
: null; : null;
_userSVipLevel = json['userSVipLevel']; _userSVipLevel = json['userSVipLevel'];
final counterJson = json['roomCounter'] ?? json['counter'];
_roomCounter =
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
_extValues = _extValues =
json['extValues'] != null json['extValues'] != null
? ExtValues.fromJson(json['extValues']) ? ExtValues.fromJson(json['extValues'])
@ -140,6 +145,7 @@ class RoomProfile {
String? _userId; String? _userId;
SocialChatUserProfile? _userProfile; SocialChatUserProfile? _userProfile;
String? _userSVipLevel; String? _userSVipLevel;
RoomMemberCounter? _roomCounter;
ExtValues? _extValues; ExtValues? _extValues;
RoomProfile copyWith({ RoomProfile copyWith({
String? countryCode, String? countryCode,
@ -159,6 +165,7 @@ class RoomProfile {
String? userId, String? userId,
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
String? userSVipLevel, String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues, ExtValues? extValues,
}) => RoomProfile( }) => RoomProfile(
countryCode: countryCode ?? _countryCode, countryCode: countryCode ?? _countryCode,
@ -178,6 +185,7 @@ class RoomProfile {
userId: userId ?? _userId, userId: userId ?? _userId,
userProfile: userProfile ?? _userProfile, userProfile: userProfile ?? _userProfile,
userSVipLevel: userSVipLevel ?? _userSVipLevel, userSVipLevel: userSVipLevel ?? _userSVipLevel,
roomCounter: roomCounter ?? _roomCounter,
extValues: extValues ?? _extValues, extValues: extValues ?? _extValues,
); );
String? get countryCode => _countryCode; String? get countryCode => _countryCode;
@ -197,8 +205,24 @@ class RoomProfile {
String? get userId => _userId; String? get userId => _userId;
SocialChatUserProfile? get userProfile => _userProfile; SocialChatUserProfile? get userProfile => _userProfile;
String? get userSVipLevel => _userSVipLevel; String? get userSVipLevel => _userSVipLevel;
RoomMemberCounter? get roomCounter => _roomCounter;
ExtValues? get extValues => _extValues; 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<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final map = <String, dynamic>{}; final map = <String, dynamic>{};
map['countryCode'] = _countryCode; map['countryCode'] = _countryCode;
@ -220,6 +244,9 @@ class RoomProfile {
map['userProfile'] = _userProfile?.toJson(); map['userProfile'] = _userProfile?.toJson();
} }
map['userSVipLevel'] = _userSVipLevel; map['userSVipLevel'] = _userSVipLevel;
if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson();
}
if (_extValues != null) { if (_extValues != null) {
map['extValues'] = _extValues?.toJson(); map['extValues'] = _extValues?.toJson();
} }

View File

@ -37,6 +37,7 @@ class SocialChatRoomRes {
String? userId, String? userId,
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
String? userSVipLevel, String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues, ExtValues? extValues,
}) { }) {
_countryCode = countryCode; _countryCode = countryCode;
@ -56,6 +57,7 @@ class SocialChatRoomRes {
_userId = userId; _userId = userId;
_userProfile = userProfile; _userProfile = userProfile;
_userSVipLevel = userSVipLevel; _userSVipLevel = userSVipLevel;
_roomCounter = roomCounter;
_extValues = extValues; _extValues = extValues;
} }
@ -85,6 +87,9 @@ class SocialChatRoomRes {
? SocialChatUserProfile.fromJson(json['userProfile']) ? SocialChatUserProfile.fromJson(json['userProfile'])
: null; : null;
_userSVipLevel = json['userSVipLevel']; _userSVipLevel = json['userSVipLevel'];
final counterJson = json['roomCounter'] ?? json['counter'];
_roomCounter =
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
_extValues = _extValues =
json['extValues'] != null json['extValues'] != null
? ExtValues.fromJson(json['extValues']) ? ExtValues.fromJson(json['extValues'])
@ -108,6 +113,7 @@ class SocialChatRoomRes {
String? _userId; String? _userId;
SocialChatUserProfile? _userProfile; SocialChatUserProfile? _userProfile;
String? _userSVipLevel; String? _userSVipLevel;
RoomMemberCounter? _roomCounter;
ExtValues? _extValues; ExtValues? _extValues;
SocialChatRoomRes copyWith({ SocialChatRoomRes copyWith({
@ -128,6 +134,7 @@ class SocialChatRoomRes {
String? userId, String? userId,
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
String? userSVipLevel, String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues, ExtValues? extValues,
}) => SocialChatRoomRes( }) => SocialChatRoomRes(
countryCode: countryCode ?? _countryCode, countryCode: countryCode ?? _countryCode,
@ -147,6 +154,7 @@ class SocialChatRoomRes {
userId: userId ?? _userId, userId: userId ?? _userId,
userProfile: userProfile ?? _userProfile, userProfile: userProfile ?? _userProfile,
userSVipLevel: userSVipLevel ?? _userSVipLevel, userSVipLevel: userSVipLevel ?? _userSVipLevel,
roomCounter: roomCounter ?? _roomCounter,
extValues: extValues ?? _extValues, extValues: extValues ?? _extValues,
); );
@ -184,8 +192,22 @@ class SocialChatRoomRes {
String? get userSVipLevel => _userSVipLevel; String? get userSVipLevel => _userSVipLevel;
RoomMemberCounter? get roomCounter => _roomCounter;
ExtValues? get extValues => _extValues; 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<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final map = <String, dynamic>{}; final map = <String, dynamic>{};
map['countryCode'] = _countryCode; map['countryCode'] = _countryCode;
@ -207,6 +229,9 @@ class SocialChatRoomRes {
map['userProfile'] = _userProfile?.toJson(); map['userProfile'] = _userProfile?.toJson();
} }
map['userSVipLevel'] = _userSVipLevel; map['userSVipLevel'] = _userSVipLevel;
if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson();
}
if (_extValues != null) { if (_extValues != null) {
map['extValues'] = _extValues?.toJson(); map['extValues'] = _extValues?.toJson();
} }
@ -214,6 +239,45 @@ class SocialChatRoomRes {
} }
} }
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<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['adminCount'] = _adminCount;
map['memberCount'] = _memberCount;
return map;
}
}
/// account : "" /// account : ""
/// accountStatus : "" /// accountStatus : ""
/// age : 0 /// age : 0

View File

@ -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<String, dynamic> toJson() {
final map = <String, dynamic>{};
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<String, dynamic> toJson() {
final map = <String, dynamic>{};
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<String, dynamic> toJson() {
final map = <String, dynamic>{};
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<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['channelCode'] = _channelCode;
map['channelName'] = _channelName;
map['channelType'] = _channelType;
map['factoryChannel'] = _factoryChannel;
map['suggestScore'] = _suggestScore;
return map;
}
}
class SCMiFaPayCommodityRes {
SCMiFaPayCommodityRes({
SCMiFaPayApplicationRes? application,
List<SCMiFaPayCommodityItemRes>? commodity,
List<SCMiFaPayChannelRes>? 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<dynamic>?)
?.map((dynamic item) => SCMiFaPayCommodityItemRes.fromJson(item))
.toList() ??
<SCMiFaPayCommodityItemRes>[];
_channels =
(json['channels'] as List<dynamic>?)
?.map((dynamic item) => SCMiFaPayChannelRes.fromJson(item))
.toList() ??
<SCMiFaPayChannelRes>[];
}
SCMiFaPayApplicationRes? _application;
List<SCMiFaPayCommodityItemRes>? _commodity;
List<SCMiFaPayChannelRes>? _channels;
SCMiFaPayApplicationRes? get application => _application;
List<SCMiFaPayCommodityItemRes>? get commodity => _commodity;
List<SCMiFaPayChannelRes>? get channels => _channels;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
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<String, dynamic> toJson() {
final map = <String, dynamic>{};
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());
}

View File

@ -1,6 +1,7 @@
import 'package:yumi/shared/business_logic/models/res/login_res.dart'; 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/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_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_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/sc_level_config_res.dart';
@ -26,15 +27,17 @@ abstract class SocialChatConfigRepository {
///. ///.
Future<CountryRes> getNoticeMessage(); Future<CountryRes> getNoticeMessage();
///
Future<List<SCTopFourWithRewardRes>> topFourWithReward();
///SudCode
Future<CountryRes> getSudCode();
///. ///.
Future<CountryRes> getCustomerService(); Future<CountryRes> getCustomerService();
///banner. ///banner.
Future<List<SCIndexBannerRes>> getBanner({List<String>?types}); Future<List<SCIndexBannerRes>> getBanner({List<String>? types});
/// ///
Future<List<SCProductConfigRes>> productConfig(); Future<List<SCProductConfigRes>> productConfig();
@ -55,11 +58,28 @@ abstract class SocialChatConfigRepository {
String? friendId, String? friendId,
}); });
///MiFaPay
Future<List<SCMiFaPayCountryRes>> mifaPayCountries();
///MiFaPay
Future<SCMiFaPayCommodityRes> mifaPayCommodity({
required String applicationId,
required String payCountryId,
String type = 'GOLD',
});
///MiFaPay
Future<SCMiFaPayRechargeRes> mifaPayRecharge({
required String applicationId,
required String goodsId,
required String payCountryId,
required String userId,
required String channelCode,
});
/// ///
Future<SCLevelConfigRes> configLevel(); Future<SCLevelConfigRes> configLevel();
///APP最新版本 ///APP最新版本
Future<SCVersionManageLatestRes> versionManageLatest(); Future<SCVersionManageLatestRes> versionManageLatest();
@ -68,5 +88,4 @@ abstract class SocialChatConfigRepository {
/// ///
Future<SocialChatUserProfile> customerService(); Future<SocialChatUserProfile> customerService();
} }

View File

@ -13,6 +13,7 @@ class SCFloatingMessage {
num? number; num? number;
num? multiple; num? multiple;
int priority = 10; // int priority = 10; //
int aggregateVersion = 0;
SCFloatingMessage({ SCFloatingMessage({
this.type = 0, this.type = 0,
@ -29,6 +30,7 @@ class SCFloatingMessage {
this.coins = 0, this.coins = 0,
this.priority = 10, this.priority = 10,
this.multiple = 10, this.multiple = 10,
this.aggregateVersion = 0,
}); });
SCFloatingMessage.fromJson(dynamic json) { SCFloatingMessage.fromJson(dynamic json) {
@ -46,6 +48,7 @@ class SCFloatingMessage {
number = json['number']; number = json['number'];
priority = json['priority']; priority = json['priority'];
multiple = json['multiple']; multiple = json['multiple'];
aggregateVersion = json['aggregateVersion'] ?? 0;
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -64,6 +67,7 @@ class SCFloatingMessage {
map['number'] = number; map['number'] = number;
map['priority'] = priority; map['priority'] = priority;
map['multiple'] = multiple; map['multiple'] = multiple;
map['aggregateVersion'] = aggregateVersion;
return map; return map;
} }
} }

View File

@ -20,12 +20,12 @@ class OverlayManager {
); );
bool _isPlaying = false; bool _isPlaying = false;
OverlayEntry? _currentOverlayEntry; OverlayEntry? _currentOverlayEntry;
SCFloatingMessage? _currentMessage;
bool _isProcessing = false; bool _isProcessing = false;
bool _isDisposed = false; bool _isDisposed = false;
static final OverlayManager _instance = static final OverlayManager _instance = OverlayManager._internal();
OverlayManager._internal();
factory OverlayManager() => _instance; factory OverlayManager() => _instance;
@ -34,16 +34,87 @@ class OverlayManager {
void addMessage(SCFloatingMessage message) { void addMessage(SCFloatingMessage message) {
if (_isDisposed) return; if (_isDisposed) return;
if (SCGlobalConfig.isFloatingAnimationInGlobal) { if (SCGlobalConfig.isFloatingAnimationInGlobal) {
if (_tryAggregateMessage(message)) {
return;
}
_messageQueue.add(message); _messageQueue.add(message);
_safeScheduleNext(); _safeScheduleNext();
} else { } else {
_messageQueue.clear(); _messageQueue.clear();
_isPlaying = false; _isPlaying = false;
_isProcessing = 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() { void _safeScheduleNext() {
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return;
_isProcessing = true; _isProcessing = true;
@ -67,7 +138,10 @@ class OverlayManager {
final messageToProcess = _messageQueue.first; final messageToProcess = _messageQueue.first;
if (messageToProcess?.type == 1) { if (messageToProcess?.type == 1) {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(context, listen: false); final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
);
if (rtcProvider.currenRoom == null) { if (rtcProvider.currenRoom == null) {
// //
_messageQueue.removeFirst(); _messageQueue.removeFirst();
@ -88,9 +162,11 @@ class OverlayManager {
void _playMessage(SCFloatingMessage message) { void _playMessage(SCFloatingMessage message) {
_isPlaying = true; _isPlaying = true;
_currentMessage = message;
final context = navigatorKey.currentState?.context; final context = navigatorKey.currentState?.context;
if (context == null || !context.mounted) { if (context == null || !context.mounted) {
_isPlaying = false; _isPlaying = false;
_currentMessage = null;
_safeScheduleNext(); _safeScheduleNext();
return; return;
} }
@ -106,7 +182,7 @@ class OverlayManager {
), ),
); );
Overlay.of(context)?.insert(_currentOverlayEntry!); Overlay.of(context).insert(_currentOverlayEntry!);
} }
Widget _buildScreenWidget(SCFloatingMessage message) { Widget _buildScreenWidget(SCFloatingMessage message) {
@ -119,10 +195,12 @@ class OverlayManager {
try { try {
_currentOverlayEntry?.remove(); _currentOverlayEntry?.remove();
_currentOverlayEntry = null; _currentOverlayEntry = null;
_currentMessage = null;
_isPlaying = false; _isPlaying = false;
_safeScheduleNext(); _safeScheduleNext();
} catch (e) { } catch (e) {
debugPrint('清理悬浮消息出错: $e'); debugPrint('清理悬浮消息出错: $e');
_currentMessage = null;
_isPlaying = false; _isPlaying = false;
_safeScheduleNext(); _safeScheduleNext();
} }
@ -131,6 +209,9 @@ class OverlayManager {
switch (message.type) { switch (message.type) {
case 0: case 0:
return FloatingLuckGiftScreenWidget( return FloatingLuckGiftScreenWidget(
key: ValueKey<String>(
'luck_${message.userId}_${message.toUserId}_${message.giftUrl}_${message.aggregateVersion}',
),
message: message, message: message,
onAnimationCompleted: onComplete, onAnimationCompleted: onComplete,
); );
@ -172,6 +253,7 @@ class OverlayManager {
_isDisposed = true; _isDisposed = true;
_currentOverlayEntry?.remove(); _currentOverlayEntry?.remove();
_currentOverlayEntry = null; _currentOverlayEntry = null;
_currentMessage = null;
_messageQueue.clear(); _messageQueue.clear();
_isPlaying = false; _isPlaying = false;
_isProcessing = false; _isProcessing = false;

View File

@ -1,4 +1,5 @@
import 'package:yumi/shared/business_logic/models/res/sc_google_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_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_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/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/country_res.dart';
@ -32,7 +33,7 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
///sys/config/banner ///sys/config/banner
@override @override
Future<List<SCIndexBannerRes>> getBanner({List<String>?types}) async { Future<List<SCIndexBannerRes>> getBanner({List<String>? types}) async {
Map<String, dynamic> params = {}; Map<String, dynamic> params = {};
if (types != null) { if (types != null) {
params["types"] = types; params["types"] = types;
@ -67,8 +68,6 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
return result; return result;
} }
///ranking/top-four-with-reward ///ranking/top-four-with-reward
@override @override
Future<List<SCTopFourWithRewardRes>> topFourWithReward() async { Future<List<SCTopFourWithRewardRes>> topFourWithReward() async {
@ -94,7 +93,6 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
return result; return result;
} }
///sys/config/banner/start-page ///sys/config/banner/start-page
@override @override
Future<List<SCStartPageRes>> getStartPage() async { Future<List<SCStartPageRes>> getStartPage() async {
@ -134,7 +132,9 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
"1585c72b88d0d249c7078aae852a0806cc3d8e483b8d861962a977a00523180a", "1585c72b88d0d249c7078aae852a0806cc3d8e483b8d861962a977a00523180a",
fromJson: fromJson:
(json) => (json) =>
(json as List).map((e) => SCProductConfigRes.fromJson(e)).toList(), (json as List)
.map((e) => SCProductConfigRes.fromJson(e))
.toList(),
); );
return result; return result;
} }
@ -185,6 +185,63 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
return result; return result;
} }
///order/web/pay/country
@override
Future<List<SCMiFaPayCountryRes>> mifaPayCountries() async {
final result = await http.get<List<SCMiFaPayCountryRes>>(
"/order/web/pay/country",
fromJson:
(json) =>
(json as List<dynamic>)
.map((dynamic item) => SCMiFaPayCountryRes.fromJson(item))
.toList(),
);
return result;
}
///order/web/pay/commodity
@override
Future<SCMiFaPayCommodityRes> mifaPayCommodity({
required String applicationId,
required String payCountryId,
String type = 'GOLD',
}) async {
final result = await http.get<SCMiFaPayCommodityRes>(
"/order/web/pay/commodity",
queryParams: {
"applicationId": applicationId,
"payCountryId": payCountryId,
"type": type,
},
fromJson: (json) => SCMiFaPayCommodityRes.fromJson(json),
);
return result;
}
///order/web/pay/recharge
@override
Future<SCMiFaPayRechargeRes> mifaPayRecharge({
required String applicationId,
required String goodsId,
required String payCountryId,
required String userId,
required String channelCode,
}) async {
final result = await http.post<SCMiFaPayRechargeRes>(
"/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 ///sys/static-config/level
@override @override
Future<SCLevelConfigRes> configLevel() async { Future<SCLevelConfigRes> configLevel() async {
@ -195,10 +252,9 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
return result; return result;
} }
///sys/version/manage/release/latest ///sys/version/manage/release/latest
@override @override
Future<SCVersionManageLatestRes> versionManageLatest() async{ Future<SCVersionManageLatestRes> versionManageLatest() async {
final result = await http.get<SCVersionManageLatestRes>( final result = await http.get<SCVersionManageLatestRes>(
"ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687", "ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687",
fromJson: (json) => SCVersionManageLatestRes.fromJson(json), fromJson: (json) => SCVersionManageLatestRes.fromJson(json),
@ -208,7 +264,7 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
///sys/version/manage/latest/review ///sys/version/manage/latest/review
@override @override
Future<VersionManageLatesReviewRes> versionManageLatestReview() async{ Future<VersionManageLatesReviewRes> versionManageLatestReview() async {
final result = await http.get<VersionManageLatesReviewRes>( final result = await http.get<VersionManageLatesReviewRes>(
"ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232", "ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232",
fromJson: (json) => VersionManageLatesReviewRes.fromJson(json), fromJson: (json) => VersionManageLatesReviewRes.fromJson(json),
@ -218,12 +274,11 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
///sys/config/customer-service ///sys/config/customer-service
@override @override
Future<SocialChatUserProfile> customerService() async{ Future<SocialChatUserProfile> customerService() async {
final result = await http.get<SocialChatUserProfile>( final result = await http.get<SocialChatUserProfile>(
"ba316258c14cc3ebddb6d28ec314bc5704e593861ca693058e9e98ab3114cf05", "ba316258c14cc3ebddb6d28ec314bc5704e593861ca693058e9e98ab3114cf05",
fromJson: (json) => SocialChatUserProfile.fromJson(json), fromJson: (json) => SocialChatUserProfile.fromJson(json),
); );
return result; return result;
} }
} }

View File

@ -13,9 +13,13 @@ import 'package:tancent_vap/widgets/vap_view.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
class SCGiftVapSvgaManager { class SCGiftVapSvgaManager {
static const int _maxPendingTaskCount = 18;
Map<String, MovieEntity> videoItemCache = {}; Map<String, MovieEntity> videoItemCache = {};
static SCGiftVapSvgaManager? _inst; static SCGiftVapSvgaManager? _inst;
static const int _maxPreloadConcurrency = 1; static const int _maxPreloadConcurrency = 1;
static const int _maxPreloadQueueLength = 12;
static const int _maxSvgaCacheEntries = 8;
static const int _maxPlayablePathCacheEntries = 24;
SCGiftVapSvgaManager._internal(); SCGiftVapSvgaManager._internal();
@ -86,7 +90,10 @@ class SCGiftVapSvgaManager {
} }
Future<void> preload(String path, {bool highPriority = false}) async { Future<void> preload(String path, {bool highPriority = false}) async {
if (path.isEmpty || _dis || _isPreloadedOrLoading(path)) { if (!SCGlobalConfig.allowsHighCostAnimations ||
path.isEmpty ||
_dis ||
_isPreloadedOrLoading(path)) {
return; return;
} }
if (highPriority) { if (highPriority) {
@ -97,12 +104,20 @@ class SCGiftVapSvgaManager {
if (_queuedPreloadPaths.contains(path)) { if (_queuedPreloadPaths.contains(path)) {
return; return;
} }
_trimPreloadQueue();
_preloadQueue.add(path); _preloadQueue.add(path);
_queuedPreloadPaths.add(path); _queuedPreloadPaths.add(path);
_log('enqueue preload path=$path queue=${_preloadQueue.length}'); _log('enqueue preload path=$path queue=${_preloadQueue.length}');
_drainPreloadQueue(); _drainPreloadQueue();
} }
void _trimPreloadQueue() {
while (_preloadQueue.length >= _maxPreloadQueueLength) {
final removedPath = _preloadQueue.removeFirst();
_queuedPreloadPaths.remove(removedPath);
}
}
void _drainPreloadQueue() { void _drainPreloadQueue() {
if (_dis || if (_dis ||
_pause || _pause ||
@ -137,7 +152,7 @@ class SCGiftVapSvgaManager {
} }
Future<MovieEntity> _loadSvgaEntity(String path) async { Future<MovieEntity> _loadSvgaEntity(String path) async {
final cached = videoItemCache[path]; final cached = _touchCachedSvgaEntity(path);
if (cached != null) { if (cached != null) {
return cached; return cached;
} }
@ -159,7 +174,9 @@ class SCGiftVapSvgaManager {
throw Exception('Unsupported SVGA path: $path'); throw Exception('Unsupported SVGA path: $path');
} }
entity.autorelease = false; entity.autorelease = false;
videoItemCache[path] = entity; if (!_dis) {
_cacheSvgaEntity(path, entity);
}
return entity; return entity;
}(); }();
_svgaLoadTasks[path] = future; _svgaLoadTasks[path] = future;
@ -175,7 +192,7 @@ class SCGiftVapSvgaManager {
if (pathType == PathType.asset || pathType == PathType.file) { if (pathType == PathType.asset || pathType == PathType.file) {
return path; return path;
} }
final cachedPath = _playablePathCache[path]; final cachedPath = _touchCachedPlayablePath(path);
if (cachedPath != null && if (cachedPath != null &&
cachedPath.isNotEmpty && cachedPath.isNotEmpty &&
File(cachedPath).existsSync()) { File(cachedPath).existsSync()) {
@ -187,7 +204,9 @@ class SCGiftVapSvgaManager {
} }
final future = () async { final future = () async {
final file = await FileCacheManager.getInstance().getFile(url: path); final file = await FileCacheManager.getInstance().getFile(url: path);
_playablePathCache[path] = file.path; if (!_dis) {
_cachePlayablePath(path, file.path);
}
return file.path; return file.path;
}(); }();
_playablePathTasks[path] = future; _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<T>(Map<String, T> cache, int maxEntries) {
while (cache.length > maxEntries) {
final oldestKey = cache.keys.first;
cache.remove(oldestKey);
}
}
void _scheduleNextTask({Duration delay = Duration.zero}) { void _scheduleNextTask({Duration delay = Duration.zero}) {
if (_dis) { if (_dis) {
return; return;
@ -301,7 +355,21 @@ class SCGiftVapSvgaManager {
customResources: customResources, 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); _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}'); _log('task enqueued path=$path queueAfter=${_tq.length}');
if (!_play) { if (!_play) {
_pn(); _pn();
@ -486,6 +554,10 @@ class SCGiftVapSvgaManager {
_log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}'); _log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}');
_dis = true; _dis = true;
stopPlayback(); stopPlayback();
_svgaLoadTasks.clear();
_playablePathTasks.clear();
videoItemCache.clear();
_playablePathCache.clear();
_rgc?.dispose(); _rgc?.dispose();
_rgc = null; _rgc = null;
_rsc?.dispose(); _rsc?.dispose();
@ -564,6 +636,11 @@ class SCPriorityQueue<E> {
void clear() => _els.clear(); void clear() => _els.clear();
E removeLast() {
if (isEmpty) throw StateError("No elements");
return _els.removeLast();
}
List<E> get unorderedElements => List.from(_els); List<E> get unorderedElements => List.from(_els);
// Iterable // Iterable

View File

@ -115,11 +115,17 @@ class SCPageListState<M, T extends SCPageList> extends State<SCPageList> {
}, },
child: child:
needLoading && isLoading needLoading && isLoading
? Center(child: CupertinoActivityIndicator(color: Colors.white24,)) ? Center(
child: CupertinoActivityIndicator(
color: Colors.white24,
),
)
: empty(), : empty(),
) )
: needLoading && isLoading : needLoading && isLoading
? Center(child: CupertinoActivityIndicator(color: Colors.white24)) ? Center(
child: CupertinoActivityIndicator(color: Colors.white24),
)
: empty()) : empty())
: _buildList(), : _buildList(),
), ),
@ -210,4 +216,10 @@ class SCPageListState<M, T extends SCPageList> extends State<SCPageList> {
) )
: Container(); : Container();
} }
@override
void dispose() {
_refreshController.dispose();
super.dispose();
}
} }

View File

@ -18,6 +18,8 @@ class RoomAnimationQueueScreen extends StatefulWidget {
} }
class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> { class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
static const int _maxAnimationQueueLength = 12;
final List<AnimationTask> _animationQueue = []; final List<AnimationTask> _animationQueue = [];
bool _isQueueProcessing = false; bool _isQueueProcessing = false;
final Map<int, GlobalKey<_RoomEntranceAnimationState>> _animationKeys = {}; final Map<int, GlobalKey<_RoomEntranceAnimationState>> _animationKeys = {};
@ -46,6 +48,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
final taskId = DateTime.now().millisecondsSinceEpoch; final taskId = DateTime.now().millisecondsSinceEpoch;
final animationKey = GlobalKey<_RoomEntranceAnimationState>(); final animationKey = GlobalKey<_RoomEntranceAnimationState>();
_trimQueueOverflow();
_animationKeys[taskId] = animationKey; _animationKeys[taskId] = animationKey;
final task = AnimationTask( final task = AnimationTask(
@ -77,6 +80,15 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
} }
} }
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}) { void _startNextAnimation({int retryCount = 0}) {
if (_animationQueue.isEmpty) return; if (_animationQueue.isEmpty) return;
@ -94,7 +106,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
}); });
} else { } else {
// //
print("动画启动失败,跳过当前任务"); debugPrint("动画启动失败,跳过当前任务");
task.onComplete(); task.onComplete();
} }
} }

View File

@ -29,6 +29,8 @@ class RoomGiftSeatFlightRequest {
} }
class RoomGiftSeatFlightController { class RoomGiftSeatFlightController {
static const int _maxBufferedRequests = 24;
static final RoomGiftSeatFlightController _instance = static final RoomGiftSeatFlightController _instance =
RoomGiftSeatFlightController._internal(); RoomGiftSeatFlightController._internal();
@ -46,6 +48,7 @@ class RoomGiftSeatFlightController {
} }
if (_state == null) { if (_state == null) {
_trimPendingRequestsOverflow();
_pendingRequests.add(normalizedRequest); _pendingRequests.add(normalizedRequest);
return; return;
} }
@ -69,6 +72,7 @@ class RoomGiftSeatFlightController {
if (_state == null) { if (_state == null) {
_trimPendingRequestsForTag(queueTag, maxTrackedRequests); _trimPendingRequestsForTag(queueTag, maxTrackedRequests);
_trimPendingRequestsOverflow();
_pendingRequests.add(normalizedRequest); _pendingRequests.add(normalizedRequest);
return; return;
} }
@ -168,6 +172,12 @@ class RoomGiftSeatFlightController {
} }
} }
void _trimPendingRequestsOverflow() {
while (_pendingRequests.length >= _maxBufferedRequests) {
_pendingRequests.removeFirst();
}
}
void _attach(_RoomGiftSeatFlightOverlayState state) { void _attach(_RoomGiftSeatFlightOverlayState state) {
_state = state; _state = state;
while (_pendingRequests.isNotEmpty) { while (_pendingRequests.isNotEmpty) {
@ -199,6 +209,8 @@ class RoomGiftSeatFlightOverlay extends StatefulWidget {
class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay> class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
static const int _maxQueuedRequests = 24;
final Queue<_QueuedRoomGiftSeatFlightRequest> _queue = Queue(); final Queue<_QueuedRoomGiftSeatFlightRequest> _queue = Queue();
final GlobalKey _overlayKey = GlobalKey(); final GlobalKey _overlayKey = GlobalKey();
@ -240,6 +252,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
void _enqueue(RoomGiftSeatFlightRequest request) { void _enqueue(RoomGiftSeatFlightRequest request) {
_ensureCenterVisual(request); _ensureCenterVisual(request);
_trimQueuedRequestsOverflow();
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request)); _queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
_scheduleNextAnimation(); _scheduleNextAnimation();
} }
@ -346,6 +359,12 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
return false; return false;
} }
void _trimQueuedRequestsOverflow() {
while (_queue.length >= _maxQueuedRequests) {
_queue.removeFirst();
}
}
void _scheduleNextAnimation() { void _scheduleNextAnimation() {
if (_isPlaying || _queue.isEmpty || !mounted) { if (_isPlaying || _queue.isEmpty || !mounted) {
return; return;

View File

@ -303,6 +303,9 @@ class _FloatingLuckGiftScreenWidgetState
], ],
), ),
textAlign: TextAlign.start, textAlign: TextAlign.start,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
strutStyle: StrutStyle( strutStyle: StrutStyle(
height: 1.1, height: 1.1,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@ -5,25 +5,37 @@ import 'package:yumi/services/audio/rtc_manager.dart';
import '../../../../modules/room/seat/sc_seat_item.dart'; import '../../../../modules/room/seat/sc_seat_item.dart';
class RoomSeatWidget extends StatefulWidget { class RoomSeatWidget extends StatefulWidget {
@override @override
_RoomSeatWidgetState createState() => _RoomSeatWidgetState(); _RoomSeatWidgetState createState() => _RoomSeatWidgetState();
} }
class _RoomSeatWidgetState extends State<RoomSeatWidget> { class _RoomSeatWidgetState extends State<RoomSeatWidget> {
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<RtcProvider>( return Consumer<RtcProvider>(
builder: (context, ref, child) { builder: (context, ref, child) {
return ref.roomWheatMap.length == 5 final int seatCount = _resolvedSeatCount(ref);
return seatCount == 5
? _buildSeat5() ? _buildSeat5()
: (ref.roomWheatMap.length == 10 : (seatCount == 10
? _buildSeat10() ? _buildSeat10()
: (ref.roomWheatMap.length == 15 : (seatCount == 15
? _buildSeat15() ? _buildSeat15()
: (ref.roomWheatMap.length == 20 : (seatCount == 20
? _buildSeat20() ? _buildSeat20()
: Container(height: 180.w)))); : Container(height: 180.w))));
}, },

View File

@ -25,6 +25,12 @@ class _RoomMicSwitchPageState extends State<RoomMicSwitchPage>
_tabController = TabController(length: _pages.length, vsync: this); _tabController = TabController(length: _pages.length, vsync: this);
} }
@override
void dispose() {
_tabController?.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_tabs.clear(); _tabs.clear();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -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` 这几组字段,便于和当前后端参数定义保持一致。 - 已按 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` 字段决定是否展示,避免再用前端本地倍率硬编码去猜。 - 已继续补齐 `GAME_LUCKY_GIFT` 的 socket 播报与中奖动效:房间群消息收到该类型后,现在会统一落聊天室中奖消息、奖励弹层队列与发送者余额回写,不再只在 `3x+` 时才触发顶部奖励动画;同时全局飘窗已改为按服务端 `globalNews` 字段决定是否展示,避免再用前端本地倍率硬编码去猜。
- 已按最新 UI 口径重排幸运礼物中奖展示:顶部 `LuckGiftNomorAnimWidget` 不再叠加大头像、倍率和奖励框,只在“单个礼物倍率 `> 10x`”或“单次中奖金币 `> 5000`”时负责播全屏 `luck_gift_reward_burst.svga`;原本的 `luck_gift_reward_frame.svga` 已改挂到房间礼物播报条最右侧,并在框内显示 `+formattedAwardAmount`,金额文案直接复用此前大头像下方那一份中奖金币额。 - 已按最新 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避免资源异常时再次只剩金额裸字。 - 已继续修正幸运礼物中奖播报条右侧奖励框不显示的问题:根因是 `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` 文字,当前已改为直接展示“中奖金额 + 金币图标 + 对应礼物图”,和房间礼物播报条的视觉表达保持一致。 - 已按最新文案口径继续收紧幸运礼物中奖消息:聊天室里高倍率幸运礼物提示不再显示冗长的 `Coins / a lucky(magic) gift` 文字,当前已改为直接展示“中奖金额 + 金币图标 + 对应礼物图”,和房间礼物播报条的视觉表达保持一致。
- 已继续微调幸运礼物中奖视觉:房间礼物播报条右侧的 `luck_gift_reward_frame.svga` 已进一步放大,并给右侧区域和主条正文预留了更宽的排版空间,避免资源已经正常显示但因为画布比例和透明边距看起来过小;同时聊天室高亮中奖消息与房间顶部幸运礼物横幅现已统一改成“中奖金额 + 金币图标 + from + 礼物图”的表达,去掉原先 `Coins / a lucky(magic) gift` 这类冗长英文文案;另外全屏 `luck_gift_reward_burst.svga` 也已补上中部金额文案,避免播发时只见特效不见本次实际中奖金币数。 - 已继续微调幸运礼物中奖视觉:房间礼物播报条右侧的 `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` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。 - 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。
- 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。 - 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。
- 已继续微调语言房个人卡片与送礼 UI个人卡片底部动作文案现已支持两行居中展示避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。 - 已继续微调语言房个人卡片与送礼 UI个人卡片底部动作文案现已支持两行居中展示避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。
@ -157,6 +162,14 @@
## 已改动文件 ## 已改动文件
- `需求进度.md` - `需求进度.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/data_sources/models/enum/sc_gift_type.dart`
- `lib/shared/tools/sc_network_image_utils.dart` - `lib/shared/tools/sc_network_image_utils.dart`
- `lib/shared/tools/sc_gift_vap_svga_manager.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/modules/home/popular/mine/sc_home_mine_page.dart`
- `lib/ui_kit/widgets/room/room_live_audio_indicator.dart` - `lib/ui_kit/widgets/room/room_live_audio_indicator.dart`
- `lib/modules/search/sc_search_page.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/headdress/store_headdress_page.dart`
- `lib/modules/store/mountains/store_mountains_page.dart` - `lib/modules/store/mountains/store_mountains_page.dart`
- `lib/modules/store/theme/store_theme_page.dart` - `lib/modules/store/theme/store_theme_page.dart`