UI更新 内存泄露优化 初步对接mifapay
This commit is contained in:
parent
f78c099a47
commit
5b607800be
@ -34,6 +34,7 @@ import 'services/auth/authentication_manager.dart';
|
||||
import 'services/gift/gift_animation_manager.dart';
|
||||
import 'services/gift/gift_system_manager.dart';
|
||||
import 'services/payment/google_payment_manager.dart';
|
||||
import 'services/payment/mifa_pay_manager.dart';
|
||||
import 'services/localization/localization_manager.dart';
|
||||
import 'services/room/rc_room_manager.dart';
|
||||
import 'services/audio/rtc_manager.dart';
|
||||
@ -261,6 +262,10 @@ class RootAppWithProviders extends StatelessWidget {
|
||||
lazy: true,
|
||||
create: (context) => IOSPaymentProcessor(),
|
||||
),
|
||||
ChangeNotifierProvider<MiFaPayManager>(
|
||||
lazy: true,
|
||||
create: (context) => MiFaPayManager(),
|
||||
),
|
||||
ChangeNotifierProvider<ShopManager>(
|
||||
lazy: true,
|
||||
create: (context) => ShopManager(),
|
||||
@ -408,8 +413,17 @@ class _YumiApplicationState extends State<YumiApplication> {
|
||||
children: [
|
||||
child ?? const SizedBox.shrink(),
|
||||
if (SCGlobalConfig.allowsHighCostAnimations)
|
||||
const Positioned.fill(
|
||||
child: VapPlusSvgaPlayer(tag: "room_gift"),
|
||||
Consumer<RtcProvider>(
|
||||
builder: (context, rtcProvider, _) {
|
||||
if (!rtcProvider.roomVisualEffectsEnabled) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return const Positioned.fill(
|
||||
child: VapPlusSvgaPlayer(
|
||||
tag: "room_gift",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned.fill(
|
||||
child: RoomGiftSeatFlightOverlay(
|
||||
|
||||
@ -55,7 +55,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(widget.type);
|
||||
final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(
|
||||
widget.type,
|
||||
);
|
||||
if (violationTypeMapping.isNotEmpty) {
|
||||
// 获取第一个值作为默认违规类型
|
||||
violationType = violationTypeMapping.values.first;
|
||||
@ -65,6 +67,12 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
imageUrls = List.generate(maxImageCount, (index) => '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
@ -166,8 +174,14 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
(widget.userProfile
|
||||
?.hasSpecialId() ??
|
||||
false)
|
||||
? _strategy.getAdminEditingIcon('specialIdBg')
|
||||
: _strategy.getAdminEditingIcon('normalIdBg'),
|
||||
? _strategy
|
||||
.getAdminEditingIcon(
|
||||
'specialIdBg',
|
||||
)
|
||||
: _strategy
|
||||
.getAdminEditingIcon(
|
||||
'normalIdBg',
|
||||
),
|
||||
),
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
@ -185,7 +199,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Image.asset(
|
||||
_strategy.getAdminEditingIcon('copyId'),
|
||||
_strategy.getAdminEditingIcon(
|
||||
'copyId',
|
||||
),
|
||||
width: 12.w,
|
||||
height: 12.w,
|
||||
),
|
||||
@ -281,7 +297,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Image.asset(
|
||||
_strategy.getAdminEditingIcon('copyId'),
|
||||
_strategy.getAdminEditingIcon(
|
||||
'copyId',
|
||||
),
|
||||
width: 12.w,
|
||||
height: 12.w,
|
||||
color: Colors.black26,
|
||||
@ -343,7 +361,7 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
children: [
|
||||
// 动态生成违规类型选项
|
||||
..._buildViolationTypeOptions(context),
|
||||
SizedBox(height: 20.w,),
|
||||
SizedBox(height: 20.w),
|
||||
Row(
|
||||
children: [
|
||||
text(
|
||||
@ -358,11 +376,13 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
SizedBox(height: 8.w),
|
||||
Container(
|
||||
padding: EdgeInsets.all(5.w),
|
||||
decoration: _strategy.getAdminEditingInputDecoration(),
|
||||
decoration:
|
||||
_strategy.getAdminEditingInputDecoration(),
|
||||
child: TextField(
|
||||
controller: _descriptionController,
|
||||
onChanged: (text) {},
|
||||
maxLength: _strategy.getAdminEditingDescriptionMaxLength(),
|
||||
maxLength:
|
||||
_strategy.getAdminEditingDescriptionMaxLength(),
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
@ -413,15 +433,23 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
Row(
|
||||
children: [
|
||||
// 动态生成图片上传组件
|
||||
...List.generate(_strategy.getAdminEditingMaxImageUploadCount(), (index) {
|
||||
...List.generate(
|
||||
_strategy
|
||||
.getAdminEditingMaxImageUploadCount(),
|
||||
(index) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
child: Stack(
|
||||
children: [
|
||||
imageUrls[index].isNotEmpty
|
||||
? netImage(url: imageUrls[index], height: 100.w)
|
||||
? netImage(
|
||||
url: imageUrls[index],
|
||||
height: 100.w,
|
||||
)
|
||||
: Image.asset(
|
||||
_strategy.getAdminEditingIcon('addPic'),
|
||||
_strategy.getAdminEditingIcon(
|
||||
'addPic',
|
||||
),
|
||||
height: 100.w,
|
||||
),
|
||||
imageUrls[index].isNotEmpty
|
||||
@ -430,7 +458,10 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
right: 5.w,
|
||||
child: GestureDetector(
|
||||
child: Image.asset(
|
||||
_strategy.getAdminEditingIcon('closePic'),
|
||||
_strategy
|
||||
.getAdminEditingIcon(
|
||||
'closePic',
|
||||
),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
),
|
||||
@ -458,7 +489,19 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
},
|
||||
),
|
||||
);
|
||||
}).expand((widget) => [widget, if (_strategy.getAdminEditingMaxImageUploadCount() > 1 && widget != Expanded) SizedBox(width: 5.w)]).toList(),
|
||||
},
|
||||
)
|
||||
.expand(
|
||||
(widget) => [
|
||||
widget,
|
||||
if (_strategy
|
||||
.getAdminEditingMaxImageUploadCount() >
|
||||
1 &&
|
||||
widget != Expanded)
|
||||
SizedBox(width: 5.w),
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15.w),
|
||||
@ -472,33 +515,44 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
debouncer.debounce(
|
||||
duration: Duration(milliseconds: 350),
|
||||
onDebounce: () {
|
||||
List<String> uploadedImages = imageUrls.where((url) => url.isNotEmpty).toList();
|
||||
List<String> uploadedImages =
|
||||
imageUrls.where((url) => url.isNotEmpty).toList();
|
||||
if (widget.type == "User") {
|
||||
SCAccountRepository().userViolationHandle(
|
||||
SCAccountRepository()
|
||||
.userViolationHandle(
|
||||
widget.userProfile?.id ?? "",
|
||||
violationType,
|
||||
1,
|
||||
_descriptionController.text,
|
||||
imageUrls: imageUrls,
|
||||
).then((b){
|
||||
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful);
|
||||
)
|
||||
.then((b) {
|
||||
SCTts.show(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.operationSuccessful,
|
||||
);
|
||||
SCNavigatorUtils.goBack(context);
|
||||
}).catchError((e){
|
||||
|
||||
});
|
||||
})
|
||||
.catchError((e) {});
|
||||
} else {
|
||||
SCChatRoomRepository().roomViolationHandle(
|
||||
SCChatRoomRepository()
|
||||
.roomViolationHandle(
|
||||
widget.roomProfile?.id ?? "",
|
||||
violationType,
|
||||
1,
|
||||
_descriptionController.text,
|
||||
imageUrls: imageUrls,
|
||||
).then((b){
|
||||
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful);
|
||||
)
|
||||
.then((b) {
|
||||
SCTts.show(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.operationSuccessful,
|
||||
);
|
||||
SCNavigatorUtils.goBack(context);
|
||||
}).catchError((e){
|
||||
|
||||
});
|
||||
})
|
||||
.catchError((e) {});
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -512,7 +566,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
gradient: LinearGradient(
|
||||
begin: AlignmentDirectional.topCenter,
|
||||
end: AlignmentDirectional.bottomCenter,
|
||||
colors: _strategy.getAdminEditingButtonGradient('warning'),
|
||||
colors: _strategy.getAdminEditingButtonGradient(
|
||||
'warning',
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
),
|
||||
@ -533,33 +589,44 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
debouncer.debounce(
|
||||
duration: Duration(milliseconds: 350),
|
||||
onDebounce: () {
|
||||
List<String> uploadedImages = imageUrls.where((url) => url.isNotEmpty).toList();
|
||||
List<String> uploadedImages =
|
||||
imageUrls.where((url) => url.isNotEmpty).toList();
|
||||
if (widget.type == "User") {
|
||||
SCAccountRepository().userViolationHandle(
|
||||
SCAccountRepository()
|
||||
.userViolationHandle(
|
||||
widget.userProfile?.id ?? "",
|
||||
violationType,
|
||||
2,
|
||||
_descriptionController.text,
|
||||
imageUrls: imageUrls,
|
||||
).then((b){
|
||||
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful);
|
||||
)
|
||||
.then((b) {
|
||||
SCTts.show(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.operationSuccessful,
|
||||
);
|
||||
SCNavigatorUtils.goBack(context);
|
||||
}).catchError((e){
|
||||
|
||||
});
|
||||
})
|
||||
.catchError((e) {});
|
||||
} else {
|
||||
SCChatRoomRepository().roomViolationHandle(
|
||||
SCChatRoomRepository()
|
||||
.roomViolationHandle(
|
||||
widget.roomProfile?.id ?? "",
|
||||
violationType,
|
||||
2,
|
||||
_descriptionController.text,
|
||||
imageUrls: imageUrls,
|
||||
).then((b){
|
||||
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful);
|
||||
)
|
||||
.then((b) {
|
||||
SCTts.show(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.operationSuccessful,
|
||||
);
|
||||
SCNavigatorUtils.goBack(context);
|
||||
}).catchError((e){
|
||||
|
||||
});
|
||||
})
|
||||
.catchError((e) {});
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -573,7 +640,9 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
gradient: LinearGradient(
|
||||
begin: AlignmentDirectional.topCenter,
|
||||
end: AlignmentDirectional.bottomCenter,
|
||||
colors: _strategy.getAdminEditingButtonGradient('adjust'),
|
||||
colors: _strategy.getAdminEditingButtonGradient(
|
||||
'adjust',
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
),
|
||||
@ -598,17 +667,23 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
}
|
||||
|
||||
List<Widget> _buildViolationTypeOptions(BuildContext context) {
|
||||
final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(widget.type);
|
||||
final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(
|
||||
widget.type,
|
||||
);
|
||||
final List<Widget> options = [];
|
||||
|
||||
if (violationTypeMapping.isEmpty) {
|
||||
// 如果没有映射,返回默认选项
|
||||
if (widget.type == "User") {
|
||||
options.add(_item(SCAppLocalizations.of(context)!.userName, 0, 1));
|
||||
options.add(_item(SCAppLocalizations.of(context)!.userProfilePicture, 1, 2));
|
||||
options.add(
|
||||
_item(SCAppLocalizations.of(context)!.userProfilePicture, 1, 2),
|
||||
);
|
||||
} else {
|
||||
options.add(_item(SCAppLocalizations.of(context)!.roomName, 0, 3));
|
||||
options.add(_item(SCAppLocalizations.of(context)!.roomProfilePicture, 1, 4));
|
||||
options.add(
|
||||
_item(SCAppLocalizations.of(context)!.roomProfilePicture, 1, 4),
|
||||
);
|
||||
options.add(_item(SCAppLocalizations.of(context)!.roomNotice, 2, 5));
|
||||
options.add(_item(SCAppLocalizations.of(context)!.roomTheme, 3, 6));
|
||||
}
|
||||
@ -677,7 +752,12 @@ class _SCEditingUserRoomPageState extends State<SCEditingUserRoomPage> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
text(str, fontSize: 13.sp, textColor: Colors.black,fontWeight: FontWeight.w500),
|
||||
text(
|
||||
str,
|
||||
fontSize: 13.sp,
|
||||
textColor: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
Spacer(),
|
||||
selectedIndex == index
|
||||
? Image.asset(
|
||||
|
||||
@ -40,6 +40,12 @@ class _SCEditRoomSearchAdminPageState extends State<SCEditRoomSearchAdminPage>
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
@ -79,17 +85,25 @@ class _SCEditRoomSearchAdminPageState extends State<SCEditRoomSearchAdminPage>
|
||||
child: searchWidget(
|
||||
hint: SCAppLocalizations.of(context)!.enterTheRoomId,
|
||||
controller: _textEditingController,
|
||||
borderColor: _strategy.getAdminSearchInputBorderColor('roomSearch'),
|
||||
textColor: _strategy.getAdminSearchInputTextColor('roomSearch'),
|
||||
borderColor: _strategy.getAdminSearchInputBorderColor(
|
||||
'roomSearch',
|
||||
),
|
||||
textColor: _strategy.getAdminSearchInputTextColor(
|
||||
'roomSearch',
|
||||
),
|
||||
),
|
||||
),
|
||||
socialchatGradientButton(
|
||||
text: SCAppLocalizations.of(context)!.search,
|
||||
radius: 25,
|
||||
textSize: 14.sp,
|
||||
textColor: _strategy.getAdminSearchButtonTextColor('roomSearch'),
|
||||
textColor: _strategy.getAdminSearchButtonTextColor(
|
||||
'roomSearch',
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: _strategy.getAdminSearchButtonGradient('roomSearch'),
|
||||
colors: _strategy.getAdminSearchButtonGradient(
|
||||
'roomSearch',
|
||||
),
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
|
||||
@ -42,6 +42,12 @@ class _SCEditUserSearchAdminPageState extends State<SCEditUserSearchAdminPage>
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
@ -81,17 +87,25 @@ class _SCEditUserSearchAdminPageState extends State<SCEditUserSearchAdminPage>
|
||||
child: searchWidget(
|
||||
hint: SCAppLocalizations.of(context)!.enterTheUserId,
|
||||
controller: _textEditingController,
|
||||
borderColor: _strategy.getAdminSearchInputBorderColor('userSearch'),
|
||||
textColor: _strategy.getAdminSearchInputTextColor('userSearch'),
|
||||
borderColor: _strategy.getAdminSearchInputBorderColor(
|
||||
'userSearch',
|
||||
),
|
||||
textColor: _strategy.getAdminSearchInputTextColor(
|
||||
'userSearch',
|
||||
),
|
||||
),
|
||||
),
|
||||
socialchatGradientButton(
|
||||
text: SCAppLocalizations.of(context)!.search,
|
||||
radius: 25,
|
||||
textSize: 14.sp,
|
||||
textColor: _strategy.getAdminSearchButtonTextColor('userSearch'),
|
||||
textColor: _strategy.getAdminSearchButtonTextColor(
|
||||
'userSearch',
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: _strategy.getAdminSearchButtonGradient('userSearch'),
|
||||
colors: _strategy.getAdminSearchButtonGradient(
|
||||
'userSearch',
|
||||
),
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
@ -190,8 +204,12 @@ class _SCEditUserSearchAdminPageState extends State<SCEditUserSearchAdminPage>
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
(data.hasSpecialId() ?? false)
|
||||
? _strategy.getAdminSearchUserInfoIcon('specialIdBg')
|
||||
: _strategy.getAdminSearchUserInfoIcon('normalIdBg'),
|
||||
? _strategy.getAdminSearchUserInfoIcon(
|
||||
'specialIdBg',
|
||||
)
|
||||
: _strategy.getAdminSearchUserInfoIcon(
|
||||
'normalIdBg',
|
||||
),
|
||||
),
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
|
||||
@ -63,6 +63,13 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
|
||||
passController.text = pwd;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
accountController.dispose();
|
||||
passController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final businessLogicStrategy = SCGlobalConfig.businessLogicStrategy;
|
||||
|
||||
@ -138,6 +138,10 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
rtmProvider?.onMessageRecvC2CReadListener = null;
|
||||
rtmProvider?.onRevokeMessageListener = null;
|
||||
rtmProvider?.onNewMessageCurrentConversationListener = null;
|
||||
_textController.dispose();
|
||||
_scrollController.dispose();
|
||||
_refreshController.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -281,7 +285,9 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
}
|
||||
|
||||
void loadFriend() {
|
||||
if (SCGlobalConfig.isSystemConversationId(currentConversation?.conversationID) ||
|
||||
if (SCGlobalConfig.isSystemConversationId(
|
||||
currentConversation?.conversationID,
|
||||
) ||
|
||||
SCGlobalConfig.isSystemUserId(currentConversation?.userID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -30,6 +30,12 @@ class _HomeEventPageState extends State<HomeEventPage>
|
||||
_loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -50,7 +56,7 @@ class _HomeEventPageState extends State<HomeEventPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8.w,),
|
||||
SizedBox(height: 8.w),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 20.w),
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
@ -22,17 +24,49 @@ class SCRoomFollowPage extends SCPageList {
|
||||
}
|
||||
|
||||
class _RoomFollowPageState
|
||||
extends SCPageListState<FollowRoomRes, SCRoomFollowPage> {
|
||||
extends SCPageListState<FollowRoomRes, SCRoomFollowPage>
|
||||
with WidgetsBindingObserver {
|
||||
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
|
||||
|
||||
String? lastId;
|
||||
Timer? _roomListRefreshTimer;
|
||||
bool _isSilentRefreshingRooms = false;
|
||||
bool _wasTickerModeEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
enablePullUp = true;
|
||||
backgroundColor = Colors.transparent;
|
||||
isGridView = true;
|
||||
gridViewCount = 2;
|
||||
loadData(1);
|
||||
_startRoomListAutoRefresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_roomListRefreshTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_refreshRoomsSilently();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final isTickerModeEnabled = TickerMode.valuesOf(context).enabled;
|
||||
if (isTickerModeEnabled && !_wasTickerModeEnabled) {
|
||||
_refreshRoomsSilently();
|
||||
}
|
||||
_wasTickerModeEnabled = isTickerModeEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -194,7 +228,7 @@ class _RoomFollowPageState
|
||||
?.isEmpty ??
|
||||
false)
|
||||
? text(
|
||||
roomRes.roomProfile?.extValues?.memberQuantity ?? "0",
|
||||
roomRes.roomProfile?.displayMemberCount ?? "0",
|
||||
fontSize: 10.sp,
|
||||
lineHeight: 1,
|
||||
)
|
||||
@ -295,4 +329,85 @@ class _RoomFollowPageState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _startRoomListAutoRefresh() {
|
||||
_roomListRefreshTimer?.cancel();
|
||||
_roomListRefreshTimer = Timer.periodic(_roomListRefreshInterval, (_) {
|
||||
_refreshRoomsSilently();
|
||||
});
|
||||
}
|
||||
|
||||
bool _canRefreshRoomListSilently() {
|
||||
return mounted &&
|
||||
TickerMode.valuesOf(context).enabled &&
|
||||
!_isSilentRefreshingRooms &&
|
||||
!isLoading;
|
||||
}
|
||||
|
||||
bool _sameRoom(FollowRoomRes previous, FollowRoomRes next) {
|
||||
return previous.roomProfile?.id == next.roomProfile?.id &&
|
||||
previous.roomProfile?.roomName == next.roomProfile?.roomName &&
|
||||
previous.roomProfile?.roomCover == next.roomProfile?.roomCover &&
|
||||
previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon &&
|
||||
previous.roomProfile?.displayMemberCount ==
|
||||
next.roomProfile?.displayMemberCount &&
|
||||
previous.roomProfile?.extValues?.roomSetting?.password ==
|
||||
next.roomProfile?.extValues?.roomSetting?.password;
|
||||
}
|
||||
|
||||
bool _sameRoomLists(List<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
@ -22,17 +24,49 @@ class SCRoomHistoryPage extends SCPageList {
|
||||
}
|
||||
|
||||
class _SCRoomHistoryPageState
|
||||
extends SCPageListState<FollowRoomRes, SCRoomHistoryPage> {
|
||||
extends SCPageListState<FollowRoomRes, SCRoomHistoryPage>
|
||||
with WidgetsBindingObserver {
|
||||
static const Duration _roomListRefreshInterval = Duration(seconds: 15);
|
||||
|
||||
String? lastId;
|
||||
Timer? _roomListRefreshTimer;
|
||||
bool _isSilentRefreshingRooms = false;
|
||||
bool _wasTickerModeEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
enablePullUp = true;
|
||||
backgroundColor = Colors.transparent;
|
||||
isGridView = true;
|
||||
gridViewCount = 2;
|
||||
loadData(1);
|
||||
_startRoomListAutoRefresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_roomListRefreshTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_refreshRoomsSilently();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final isTickerModeEnabled = TickerMode.valuesOf(context).enabled;
|
||||
if (isTickerModeEnabled && !_wasTickerModeEnabled) {
|
||||
_refreshRoomsSilently();
|
||||
}
|
||||
_wasTickerModeEnabled = isTickerModeEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -195,7 +229,7 @@ class _SCRoomHistoryPageState
|
||||
?.isEmpty ??
|
||||
false)
|
||||
? text(
|
||||
roomRes.roomProfile?.extValues?.memberQuantity ?? "0",
|
||||
roomRes.roomProfile?.displayMemberCount ?? "0",
|
||||
fontSize: 10.sp,
|
||||
lineHeight: 1,
|
||||
)
|
||||
@ -295,4 +329,85 @@ class _SCRoomHistoryPageState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _startRoomListAutoRefresh() {
|
||||
_roomListRefreshTimer?.cancel();
|
||||
_roomListRefreshTimer = Timer.periodic(_roomListRefreshInterval, (_) {
|
||||
_refreshRoomsSilently();
|
||||
});
|
||||
}
|
||||
|
||||
bool _canRefreshRoomListSilently() {
|
||||
return mounted &&
|
||||
TickerMode.valuesOf(context).enabled &&
|
||||
!_isSilentRefreshingRooms &&
|
||||
!isLoading;
|
||||
}
|
||||
|
||||
bool _sameRoom(FollowRoomRes previous, FollowRoomRes next) {
|
||||
return previous.roomProfile?.id == next.roomProfile?.id &&
|
||||
previous.roomProfile?.roomName == next.roomProfile?.roomName &&
|
||||
previous.roomProfile?.roomCover == next.roomProfile?.roomCover &&
|
||||
previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon &&
|
||||
previous.roomProfile?.displayMemberCount ==
|
||||
next.roomProfile?.displayMemberCount &&
|
||||
previous.roomProfile?.extValues?.roomSetting?.password ==
|
||||
next.roomProfile?.extValues?.roomSetting?.password;
|
||||
}
|
||||
|
||||
bool _sameRoomLists(List<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,17 +85,18 @@ class _HomeMinePageState extends State<SCHomeMinePage>
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
labelColor: SocialChatTheme.primaryLight,
|
||||
isScrollable: true,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
indicator: BoxDecoration(),
|
||||
unselectedLabelColor: Colors.white,
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
fontFamily: 'MyCustomFont',
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontSize: 19.sp,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
fontFamily: 'MyCustomFont',
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
indicatorColor: Colors.transparent,
|
||||
dividerColor: Colors.transparent,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
@ -17,6 +18,7 @@ import '../../../../services/general/sc_app_general_manager.dart';
|
||||
import '../../../../ui_kit/components/sc_compontent.dart';
|
||||
import '../../../../ui_kit/components/text/sc_text.dart';
|
||||
import '../../../../ui_kit/widgets/room/room_live_audio_indicator.dart';
|
||||
import '../../../../ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
import '../../../index/main_route.dart';
|
||||
|
||||
const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450);
|
||||
@ -34,7 +36,16 @@ class SCHomePartyPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
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 = [];
|
||||
String? lastId;
|
||||
bool isLoading = false;
|
||||
@ -46,24 +57,50 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
late final AnimationController _skeletonController;
|
||||
List<SocialChatRoomRes> rooms = [];
|
||||
int _currentIndex = 0;
|
||||
Timer? _roomListRefreshTimer;
|
||||
bool _isSilentRefreshingRooms = false;
|
||||
bool _isHydratingVisibleRoomCounters = false;
|
||||
bool _wasTickerModeEnabled = false;
|
||||
DateTime? _lastRoomCounterHydrationAt;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_skeletonController = AnimationController(
|
||||
vsync: this,
|
||||
duration: _kPartySkeletonAnimationDuration,
|
||||
)..repeat();
|
||||
loadData();
|
||||
_startRoomListAutoRefresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_roomListRefreshTimer?.cancel();
|
||||
_skeletonController.dispose();
|
||||
_refreshController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_refreshRoomsSilently();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final isTickerModeEnabled = TickerMode.valuesOf(context).enabled;
|
||||
if (isTickerModeEnabled && !_wasTickerModeEnabled) {
|
||||
_refreshRoomsSilently();
|
||||
}
|
||||
_wasTickerModeEnabled = isTickerModeEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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) {
|
||||
final banners = _partyBanners(ref);
|
||||
final hasMultipleBanners = banners.length > 1;
|
||||
@ -774,11 +975,14 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
SCChatRoomRepository()
|
||||
.discovery(allRegion: true)
|
||||
.then((values) {
|
||||
rooms = values;
|
||||
rooms = _mergeLatestRoomsWithCurrentCounters(values);
|
||||
isLoading = false;
|
||||
_refreshController.refreshCompleted();
|
||||
_refreshController.loadComplete();
|
||||
if (mounted) setState(() {});
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
unawaited(_syncVisibleRoomMemberCounts(force: true));
|
||||
}
|
||||
})
|
||||
.catchError((e) {
|
||||
_refreshController.loadNoData();
|
||||
@ -805,6 +1009,11 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
}
|
||||
|
||||
_buildItem(SocialChatRoomRes res, int index) {
|
||||
final rankBorderAsset =
|
||||
index < _topRankBorderAssets.length ? _topRankBorderAssets[index] : null;
|
||||
final rankBorderOverflow = 12.w;
|
||||
final rankInfoHorizontalInset = rankBorderAsset != null ? 10.w : 0.w;
|
||||
final rankInfoBottomInset = rankBorderAsset != null ? 9.w : 0.w;
|
||||
return SCDebounceWidget(
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
||||
@ -813,6 +1022,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Container(
|
||||
@ -831,7 +1041,13 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
height: 200.w,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: rankInfoHorizontalInset,
|
||||
right: rankInfoHorizontalInset,
|
||||
bottom: rankInfoBottomInset,
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 6.w),
|
||||
margin: EdgeInsets.symmetric(horizontal: 1.w),
|
||||
decoration: BoxDecoration(
|
||||
@ -920,7 +1136,7 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
: Container(height: 10.w),
|
||||
(res.extValues?.roomSetting?.password?.isEmpty ?? false)
|
||||
? text(
|
||||
res.extValues?.memberQuantity ?? "0",
|
||||
res.displayMemberCount,
|
||||
fontSize: 10.sp,
|
||||
lineHeight: 1,
|
||||
)
|
||||
@ -929,6 +1145,21 @@ class _HomePartyPageState extends State<SCHomePartyPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -36,9 +36,16 @@ class _ReportPageState extends State<ReportPage> {
|
||||
String pic03 = "";
|
||||
final TextEditingController _descriptionController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCDebounceWidget(child: Stack(
|
||||
return SCDebounceWidget(
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getReportPageBackgroundImage(),
|
||||
@ -68,13 +75,19 @@ class _ReportPageState extends State<ReportPage> {
|
||||
context,
|
||||
)!.pleaseSelectTheTypeContent,
|
||||
fontSize: 14.sp,
|
||||
textColor: SCGlobalConfig.businessLogicStrategy.getReportPageHintTextColor(),
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPageHintTextColor(),
|
||||
),
|
||||
SizedBox(width: 15.w),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 10.w, left: 15.w, right: 15.w),
|
||||
margin: EdgeInsets.only(
|
||||
top: 10.w,
|
||||
left: 15.w,
|
||||
right: 15.w,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 15.w),
|
||||
child: Column(
|
||||
children: [
|
||||
@ -90,7 +103,9 @@ class _ReportPageState extends State<ReportPage> {
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.description,
|
||||
fontSize: 14.sp,
|
||||
textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor(),
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPagePrimaryTextColor(),
|
||||
),
|
||||
Spacer(),
|
||||
],
|
||||
@ -100,7 +115,9 @@ class _ReportPageState extends State<ReportPage> {
|
||||
padding: EdgeInsets.all(5.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: SCGlobalConfig.businessLogicStrategy.getReportPageContainerBackgroundColor(),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPageContainerBackgroundColor(),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _descriptionController,
|
||||
@ -109,9 +126,13 @@ class _ReportPageState extends State<ReportPage> {
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
SCAppLocalizations.of(context)!.reportInputTips,
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.reportInputTips,
|
||||
hintStyle: TextStyle(
|
||||
color: SCGlobalConfig.businessLogicStrategy.getReportPageSecondaryHintTextColor(),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPageSecondaryHintTextColor(),
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
contentPadding: EdgeInsets.only(top: 0.w),
|
||||
@ -131,11 +152,13 @@ class _ReportPageState extends State<ReportPage> {
|
||||
// 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)
|
||||
counterStyle: TextStyle(color: Colors.white),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: sp(15),
|
||||
color: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor(),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPagePrimaryTextColor(),
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
),
|
||||
),
|
||||
@ -146,7 +169,9 @@ class _ReportPageState extends State<ReportPage> {
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.screenshotTips,
|
||||
fontSize: 14.sp,
|
||||
textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor(),
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPagePrimaryTextColor(),
|
||||
),
|
||||
Spacer(),
|
||||
],
|
||||
@ -164,7 +189,8 @@ class _ReportPageState extends State<ReportPage> {
|
||||
height: 100.w,
|
||||
)
|
||||
: Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getReportPageIcon('addPic'),
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPageIcon('addPic'),
|
||||
height: 100.w,
|
||||
),
|
||||
pic01.isNotEmpty
|
||||
@ -173,7 +199,11 @@ class _ReportPageState extends State<ReportPage> {
|
||||
right: 5.w,
|
||||
child: GestureDetector(
|
||||
child: Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getReportPageIcon('closePic'),
|
||||
SCGlobalConfig
|
||||
.businessLogicStrategy
|
||||
.getReportPageIcon(
|
||||
'closePic',
|
||||
),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
),
|
||||
@ -212,7 +242,8 @@ class _ReportPageState extends State<ReportPage> {
|
||||
height: 100.w,
|
||||
)
|
||||
: Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getReportPageIcon('addPic'),
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPageIcon('addPic'),
|
||||
height: 100.w,
|
||||
),
|
||||
pic02.isNotEmpty
|
||||
@ -221,7 +252,11 @@ class _ReportPageState extends State<ReportPage> {
|
||||
right: 5.w,
|
||||
child: GestureDetector(
|
||||
child: Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getReportPageIcon('closePic'),
|
||||
SCGlobalConfig
|
||||
.businessLogicStrategy
|
||||
.getReportPageIcon(
|
||||
'closePic',
|
||||
),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
),
|
||||
@ -260,7 +295,8 @@ class _ReportPageState extends State<ReportPage> {
|
||||
height: 100.w,
|
||||
)
|
||||
: Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getReportPageIcon('addPic'),
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPageIcon('addPic'),
|
||||
height: 100.w,
|
||||
),
|
||||
pic03.isNotEmpty
|
||||
@ -269,7 +305,11 @@ class _ReportPageState extends State<ReportPage> {
|
||||
right: 5.w,
|
||||
child: GestureDetector(
|
||||
child: Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getReportPageIcon('closePic'),
|
||||
SCGlobalConfig
|
||||
.businessLogicStrategy
|
||||
.getReportPageIcon(
|
||||
'closePic',
|
||||
),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
),
|
||||
@ -319,7 +359,10 @@ class _ReportPageState extends State<ReportPage> {
|
||||
}
|
||||
SCChatRoomRepository()
|
||||
.reported(
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id ??
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.id ??
|
||||
"",
|
||||
widget.tageId,
|
||||
selectedIndex,
|
||||
@ -336,7 +379,6 @@ class _ReportPageState extends State<ReportPage> {
|
||||
.catchError((_) {
|
||||
SCLoadingManager.hide();
|
||||
});
|
||||
|
||||
},
|
||||
child: Container(
|
||||
height: 40.w,
|
||||
@ -344,12 +386,19 @@ class _ReportPageState extends State<ReportPage> {
|
||||
alignment: Alignment.center,
|
||||
margin: EdgeInsets.symmetric(horizontal: 35.w),
|
||||
decoration: BoxDecoration(
|
||||
color: SCGlobalConfig.businessLogicStrategy.getReportPageSubmitButtonBackgroundColor(),
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPageSubmitButtonBackgroundColor(),
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
),
|
||||
child: Text(
|
||||
SCAppLocalizations.of(context)!.submit,
|
||||
style: TextStyle(color: SCGlobalConfig.businessLogicStrategy.getReportPageSubmitButtonTextColor(), fontSize: 16.sp),
|
||||
style: TextStyle(
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPageSubmitButtonTextColor(),
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -360,9 +409,11 @@ class _ReportPageState extends State<ReportPage> {
|
||||
),
|
||||
),
|
||||
],
|
||||
), onTap: (){
|
||||
),
|
||||
onTap: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _item(String str, int index) {
|
||||
@ -381,11 +432,19 @@ class _ReportPageState extends State<ReportPage> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
text(str, fontSize: 14.sp, textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor()),
|
||||
text(
|
||||
str,
|
||||
fontSize: 14.sp,
|
||||
textColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPagePrimaryTextColor(),
|
||||
),
|
||||
Spacer(),
|
||||
selectedIndex == index
|
||||
? Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy.getReportPageIcon('checked'),
|
||||
SCGlobalConfig.businessLogicStrategy.getReportPageIcon(
|
||||
'checked',
|
||||
),
|
||||
width: 20.w,
|
||||
fit: BoxFit.fitWidth,
|
||||
)
|
||||
@ -394,7 +453,12 @@ class _ReportPageState extends State<ReportPage> {
|
||||
height: 20.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getReportPageUnselectedBorderColor(), width: 2.w),
|
||||
border: Border.all(
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getReportPageUnselectedBorderColor(),
|
||||
width: 2.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -78,6 +78,13 @@ class _RoomEditPageState extends State<RoomEditPage> {
|
||||
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomCover ?? "";
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_roomNameController.dispose();
|
||||
_roomAnnouncementController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
|
||||
@ -39,6 +39,12 @@ class _RoomGiftRankPageState extends State<RoomGiftRankPage>
|
||||
_tabController = TabController(length: _pages.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_tabs.clear();
|
||||
@ -97,5 +103,4 @@ class _RoomGiftRankPageState extends State<RoomGiftRankPage>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -33,6 +33,8 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
RtcProvider? provider;
|
||||
JoinRoomRes? room;
|
||||
MicRes? roomSeat;
|
||||
JoinRoomRes? _cachedRoom;
|
||||
MicRes? _cachedRoomSeat;
|
||||
final GlobalKey _targetKey = GlobalKey();
|
||||
|
||||
@override
|
||||
@ -44,8 +46,22 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
roomSeat = provider?.roomWheatMap[widget.index];
|
||||
room = provider?.currenRoom;
|
||||
final liveRoomSeat = provider?.roomWheatMap[widget.index];
|
||||
final liveRoom = provider?.currenRoom;
|
||||
if (provider?.isExitingCurrentVoiceRoomSession ?? false) {
|
||||
roomSeat = liveRoomSeat ?? _cachedRoomSeat;
|
||||
room = liveRoom ?? _cachedRoom;
|
||||
} else {
|
||||
roomSeat = liveRoomSeat;
|
||||
room = liveRoom;
|
||||
if (roomSeat != null) {
|
||||
_cachedRoomSeat = roomSeat;
|
||||
}
|
||||
if (room != null) {
|
||||
_cachedRoom = room;
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
@ -78,7 +94,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
? ""
|
||||
: roomSeat?.user?.getHeaddress()?.sourceUrl,
|
||||
)
|
||||
: (roomSeat!.micLock!
|
||||
: ((roomSeat?.micLock ?? false)
|
||||
? Image.asset(
|
||||
"sc_images/room/sc_icon_seat_lock.png",
|
||||
width: widget.isGameModel ? 38.w : 52.w,
|
||||
@ -93,7 +109,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
bottom: widget.isGameModel ? 2.w : 5.w,
|
||||
right: widget.isGameModel ? 2.w : 5.w,
|
||||
child:
|
||||
roomSeat!.micMute!
|
||||
(roomSeat?.micMute ?? false)
|
||||
? Image.asset(
|
||||
"sc_images/room/sc_icon_room_seat_mic_mute.png",
|
||||
width: 14.w,
|
||||
|
||||
@ -2,7 +2,6 @@ import 'dart:math' as math;
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
@ -53,7 +52,6 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
|
||||
late TabController _tabController;
|
||||
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
|
||||
final List<Widget> _tabs = [];
|
||||
late StreamSubscription _subscription;
|
||||
final RoomGiftSeatFlightController _giftSeatFlightController =
|
||||
RoomGiftSeatFlightController();
|
||||
@ -65,7 +63,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
super.initState();
|
||||
_tabController = TabController(length: _pages.length, vsync: this);
|
||||
_enableRoomVisualEffects();
|
||||
_tabController.addListener(() {}); // 监听切换
|
||||
_tabController.addListener(_handleTabChange);
|
||||
|
||||
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
||||
event,
|
||||
@ -91,11 +89,19 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
@override
|
||||
void dispose() {
|
||||
_suspendRoomVisualEffects();
|
||||
_tabController.removeListener(_handleTabChange);
|
||||
_tabController.dispose(); // 释放资源
|
||||
_subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTabChange() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _enableRoomVisualEffects() {
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
@ -150,10 +156,6 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_tabs.clear();
|
||||
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.all));
|
||||
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.chat));
|
||||
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.gift));
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) {
|
||||
@ -236,20 +238,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
children: [
|
||||
TabBar(
|
||||
tabAlignment: TabAlignment.start,
|
||||
labelPadding: SCGlobalConfig.businessLogicStrategy
|
||||
.getVoiceRoomTabLabelPadding()
|
||||
.copyWith(
|
||||
left:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getVoiceRoomTabLabelPadding()
|
||||
.left *
|
||||
ScreenUtil().setWidth(1),
|
||||
right:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getVoiceRoomTabLabelPadding()
|
||||
.right *
|
||||
ScreenUtil().setWidth(1),
|
||||
),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||
labelColor:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getVoiceRoomTabLabelColor(),
|
||||
@ -283,7 +274,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getVoiceRoomTabDividerColor(),
|
||||
controller: _tabController,
|
||||
tabs: _tabs,
|
||||
tabs: List<Widget>.generate(_pages.length, _buildImageTab),
|
||||
),
|
||||
Expanded(
|
||||
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) {
|
||||
if (!Provider.of<RtcProvider>(
|
||||
|
||||
@ -548,7 +548,7 @@ class _SearchRoomListState extends State<SearchRoomList> {
|
||||
: Container(height: 10.w),
|
||||
(e.extValues?.roomSetting?.password?.isEmpty ?? false)
|
||||
? text(
|
||||
e.extValues?.memberQuantity ?? "0",
|
||||
e.displayMemberCount,
|
||||
fontSize: 10.sp,
|
||||
lineHeight: 1,
|
||||
)
|
||||
|
||||
@ -29,6 +29,12 @@ class _LevelPageState extends State<LevelPage>
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
|
||||
@ -7,8 +7,6 @@ import 'package:yumi/modules/user/my_items/theme/bags_theme_page.dart';
|
||||
|
||||
///背包-房间主题
|
||||
class BagsTabThemePage extends StatefulWidget {
|
||||
|
||||
|
||||
@override
|
||||
_BagsTabThemePageState createState() => _BagsTabThemePageState();
|
||||
}
|
||||
@ -22,14 +20,18 @@ class _BagsTabThemePageState extends State<BagsTabThemePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pages.add(
|
||||
BagsThemePage(),
|
||||
);
|
||||
_pages.add(BagsThemePage());
|
||||
_pages.add(RoomThemeCustomPage());
|
||||
_tabController = TabController(length: _pages.length, vsync: this);
|
||||
_tabController.addListener(() {}); // 监听切换
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_tabs.clear();
|
||||
|
||||
@ -160,6 +160,13 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
||||
return Tab(text: text);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildProfileHeader(
|
||||
SocialChatUserProfileManager ref,
|
||||
List<PersonPhoto> backgroundPhotos,
|
||||
|
||||
@ -33,6 +33,14 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
///旧密码
|
||||
TextEditingController oldPassController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
confirmController.dispose();
|
||||
passController.dispose();
|
||||
oldPassController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
@ -50,7 +58,9 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
title: SCAppLocalizations.of(context)!.resetLoginPassword,
|
||||
actions: [],
|
||||
),
|
||||
body: SafeArea(top: false,child: Column(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
@ -75,14 +85,18 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
),
|
||||
Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
|
||||
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)
|
||||
color: Color(0xff18F2B1).withOpacity(0.1),
|
||||
),
|
||||
child: text(
|
||||
AccountStorage().getCurrentUser()?.userProfile?.account ?? "",
|
||||
AccountStorage().getCurrentUser()?.userProfile?.account ??
|
||||
"",
|
||||
textColor: Colors.white54,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
@ -100,18 +114,21 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
),
|
||||
Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
|
||||
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)
|
||||
color: Color(0xff18F2B1).withOpacity(0.1),
|
||||
),
|
||||
child: TextField(
|
||||
controller: oldPassController,
|
||||
maxLength: 30,
|
||||
maxLines: 1,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s'))
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
],
|
||||
|
||||
obscureText: !showPass3,
|
||||
@ -174,18 +191,21 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
),
|
||||
Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
|
||||
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)
|
||||
color: Color(0xff18F2B1).withOpacity(0.1),
|
||||
),
|
||||
child: TextField(
|
||||
controller: passController,
|
||||
maxLength: 30,
|
||||
maxLines: 1,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s'))
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
],
|
||||
|
||||
obscureText: !showPass,
|
||||
@ -237,7 +257,10 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
),
|
||||
Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10.w,
|
||||
vertical: 10.w,
|
||||
),
|
||||
margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@ -248,7 +271,7 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
maxLength: 30,
|
||||
maxLines: 1,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s'))
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
],
|
||||
|
||||
obscureText: !showPass2,
|
||||
@ -257,7 +280,8 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
setState(() {});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: SCAppLocalizations.of(context)!.confirmYourPassword,
|
||||
hintText:
|
||||
SCAppLocalizations.of(context)!.confirmYourPassword,
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white54,
|
||||
fontSize: 14.sp,
|
||||
@ -302,7 +326,9 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.resetLoginPasswordtTips2,
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.resetLoginPasswordtTips2,
|
||||
fontSize: 10.sp,
|
||||
textColor: Colors.white,
|
||||
maxLines: 3,
|
||||
@ -332,7 +358,8 @@ class _ResetPwdPageState extends State<ResetPwdPage> {
|
||||
},
|
||||
),
|
||||
],
|
||||
),),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@ -29,6 +29,13 @@ class _SetPwdPageState extends State<SetPwdPage> {
|
||||
///密码控制器
|
||||
TextEditingController passController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
confirmController.dispose();
|
||||
passController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
|
||||
@ -45,6 +45,12 @@ class _UserBlockedListPageState
|
||||
loadData(1);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
@ -118,13 +124,15 @@ class _UserBlockedListPageState
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 10.w),
|
||||
SCDebounceWidget(child: head(url: res.userProfile?.userAvatar ?? "", width: 62.w), onTap: (){
|
||||
SCDebounceWidget(
|
||||
child: head(url: res.userProfile?.userAvatar ?? "", width: 62.w),
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.userProfile?.id}&tageId=${res.userProfile?.id}",
|
||||
);
|
||||
})
|
||||
,
|
||||
},
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -134,7 +142,10 @@ class _UserBlockedListPageState
|
||||
children: [
|
||||
netImage(
|
||||
url:
|
||||
Provider.of<SCAppGeneralManager>(context, listen: false)
|
||||
Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
)
|
||||
.findCountryByName(
|
||||
res.userProfile?.countryName ?? "",
|
||||
)
|
||||
@ -150,14 +161,15 @@ class _UserBlockedListPageState
|
||||
textColor: Colors.white,
|
||||
res.userProfile?.userNickname ?? "",
|
||||
fontSize: 14.sp,
|
||||
type:res.userProfile?.getVIP()?.name ?? "",
|
||||
type: res.userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(res.userProfile?.userNickname?.characters.length ?? 0) >
|
||||
(res.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
10,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
Container(
|
||||
width: (res.userProfile?.age??0)>999?58.w:48.w,
|
||||
width: (res.userProfile?.age ?? 0) > 999 ? 58.w : 48.w,
|
||||
height: 24.w,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
@ -234,13 +246,15 @@ class _UserBlockedListPageState
|
||||
],
|
||||
),
|
||||
),
|
||||
SCDebounceWidget(child: Image.asset(
|
||||
SCDebounceWidget(
|
||||
child: Image.asset(
|
||||
"sc_images/room/sc_icon_block_list_delete.png",
|
||||
height: 20.w,
|
||||
), onTap: (){
|
||||
),
|
||||
onTap: () {
|
||||
_delete(res.userProfile);
|
||||
})
|
||||
,
|
||||
},
|
||||
),
|
||||
SizedBox(width: 15.w),
|
||||
],
|
||||
),
|
||||
@ -307,34 +321,19 @@ class _UserBlockedListPageState
|
||||
void _delete(SocialChatUserProfile? userProfile) {
|
||||
SmartDialog.show(
|
||||
tag: "showConfirmDialog",
|
||||
alignment:
|
||||
Alignment.center,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType:
|
||||
SmartAnimationType
|
||||
.fade,
|
||||
animationType: SmartAnimationType.fade,
|
||||
builder: (_) {
|
||||
return MsgDialog(
|
||||
title:
|
||||
SCAppLocalizations.of(
|
||||
context!,
|
||||
)!.tips,
|
||||
msg:SCAppLocalizations.of(
|
||||
context,
|
||||
)!.areYouSureToCancelBlacklist,
|
||||
btnText:
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.confirm,
|
||||
title: SCAppLocalizations.of(context!)!.tips,
|
||||
msg: SCAppLocalizations.of(context)!.areYouSureToCancelBlacklist,
|
||||
btnText: SCAppLocalizations.of(context)!.confirm,
|
||||
onEnsure: () {
|
||||
SCLoadingManager.show();
|
||||
SCAccountRepository()
|
||||
.deleteUserBlacklist(
|
||||
userProfile?.id??"",
|
||||
)
|
||||
.then((
|
||||
result,
|
||||
) {
|
||||
.deleteUserBlacklist(userProfile?.id ?? "")
|
||||
.then((result) {
|
||||
SCTts.show(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
@ -342,9 +341,7 @@ class _UserBlockedListPageState
|
||||
);
|
||||
loadData(1);
|
||||
})
|
||||
.catchError((
|
||||
e,
|
||||
) {
|
||||
.catchError((e) {
|
||||
SCLoadingManager.hide();
|
||||
});
|
||||
},
|
||||
|
||||
120
lib/modules/wallet/recharge/mifa_pay_webview_page.dart
Normal file
120
lib/modules/wallet/recharge/mifa_pay_webview_page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
463
lib/modules/wallet/recharge/recharge_method_bottom_sheet.dart
Normal file
463
lib/modules/wallet/recharge/recharge_method_bottom_sheet.dart
Normal 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
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
import 'package:fluro/fluro.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@ -44,8 +46,16 @@ typedef OnSoundVoiceChange = Function(num index, int volum);
|
||||
typedef RtcProvider = RealTimeCommunicationManager;
|
||||
|
||||
class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||||
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
|
||||
|
||||
bool needUpDataUserInfo = false;
|
||||
bool _roomVisualEffectsEnabled = false;
|
||||
bool _isExitingCurrentVoiceRoomSession = false;
|
||||
Timer? _micListPollingTimer;
|
||||
Timer? _onlineUsersPollingTimer;
|
||||
bool _isRefreshingMicList = false;
|
||||
bool _isRefreshingOnlineUsers = false;
|
||||
|
||||
///当前所在房间
|
||||
JoinRoomRes? currenRoom;
|
||||
@ -91,6 +101,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled;
|
||||
|
||||
bool get isExitingCurrentVoiceRoomSession =>
|
||||
_isExitingCurrentVoiceRoomSession;
|
||||
|
||||
bool get shouldShowRoomVisualEffects =>
|
||||
currenRoom != null && _roomVisualEffectsEnabled;
|
||||
|
||||
@ -102,6 +115,186 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setExitingCurrentVoiceRoomSession(bool enabled, {bool notify = true}) {
|
||||
if (_isExitingCurrentVoiceRoomSession == enabled) {
|
||||
return;
|
||||
}
|
||||
_isExitingCurrentVoiceRoomSession = enabled;
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _startRoomStatePolling() {
|
||||
_stopRoomStatePolling();
|
||||
if ((currenRoom?.roomProfile?.roomProfile?.id ?? "").isEmpty) {
|
||||
return;
|
||||
}
|
||||
_micListPollingTimer = Timer.periodic(_micListPollingInterval, (_) {
|
||||
retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {});
|
||||
});
|
||||
_onlineUsersPollingTimer = Timer.periodic(_onlineUsersPollingInterval, (_) {
|
||||
fetchOnlineUsersList(notifyIfUnchanged: false).catchError((_) {});
|
||||
});
|
||||
}
|
||||
|
||||
void _stopRoomStatePolling() {
|
||||
_micListPollingTimer?.cancel();
|
||||
_onlineUsersPollingTimer?.cancel();
|
||||
_micListPollingTimer = null;
|
||||
_onlineUsersPollingTimer = null;
|
||||
_isRefreshingMicList = false;
|
||||
_isRefreshingOnlineUsers = false;
|
||||
}
|
||||
|
||||
bool _sameOnlineUsers(
|
||||
List<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 {
|
||||
try {
|
||||
engine = await _initAgoraRtcEngine();
|
||||
@ -343,6 +536,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
listen: false,
|
||||
).fetchUserProfileData();
|
||||
retrieveMicrophoneList();
|
||||
fetchOnlineUsersList();
|
||||
_startRoomStatePolling();
|
||||
setRoomVisualEffectsEnabled(true);
|
||||
SCFloatIchart().remove();
|
||||
SCNavigatorUtils.push(
|
||||
@ -453,6 +648,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
///获取麦位
|
||||
retrieveMicrophoneList();
|
||||
fetchOnlineUsersList();
|
||||
_startRoomStatePolling();
|
||||
Provider.of<SocialChatRoomManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
@ -510,46 +707,58 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
///获取在线用户
|
||||
Future fetchOnlineUsersList() async {
|
||||
onlineUsers = await SCChatRoomRepository().roomOnlineUsers(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
);
|
||||
managerUsers.clear();
|
||||
for (var user in onlineUsers) {
|
||||
if (user.roles == SCRoomRolesType.ADMIN.name) {
|
||||
managerUsers.add(user);
|
||||
Future<void> fetchOnlineUsersList({bool notifyIfUnchanged = true}) async {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
if (roomId.isEmpty || _isRefreshingOnlineUsers) {
|
||||
return;
|
||||
}
|
||||
_isRefreshingOnlineUsers = true;
|
||||
try {
|
||||
final fetchedUsers = await SCChatRoomRepository().roomOnlineUsers(roomId);
|
||||
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
|
||||
return;
|
||||
}
|
||||
final changed = !_sameOnlineUsers(onlineUsers, fetchedUsers);
|
||||
onlineUsers = fetchedUsers;
|
||||
_refreshManagerUsers(onlineUsers);
|
||||
if (changed || notifyIfUnchanged) {
|
||||
notifyListeners();
|
||||
}
|
||||
} finally {
|
||||
_isRefreshingOnlineUsers = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future retrieveMicrophoneList() async {
|
||||
Future<void> retrieveMicrophoneList({bool notifyIfUnchanged = true}) async {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
if (roomId.isEmpty || _isRefreshingMicList) {
|
||||
return;
|
||||
}
|
||||
_isRefreshingMicList = true;
|
||||
bool isOnMic = false;
|
||||
var roomWheatList = await SCChatRoomRepository().micList(
|
||||
currenRoom!.roomProfile?.roomProfile?.id ?? "",
|
||||
);
|
||||
for (var roomWheat in roomWheatList) {
|
||||
roomWheatMap[roomWheat.micIndex!] = roomWheat;
|
||||
try {
|
||||
final roomWheatList = await SCChatRoomRepository().micList(roomId);
|
||||
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
|
||||
return;
|
||||
}
|
||||
final nextMap = _buildMicMap(roomWheatList);
|
||||
for (final roomWheat in nextMap.values) {
|
||||
if (roomWheat.user != null &&
|
||||
roomWheat.user!.id ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
isOnMic = true;
|
||||
|
||||
///自己在麦上
|
||||
SCHeartbeatUtils.scheduleAnchorHeartbeat(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
_applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged);
|
||||
SCHeartbeatUtils.scheduleHeartbeat(
|
||||
SCHeartbeatStatus.VOICE_LIVE.name,
|
||||
isOnMic,
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
);
|
||||
Future.delayed(Duration(milliseconds: 1500), () {
|
||||
fetchOnlineUsersList();
|
||||
});
|
||||
} finally {
|
||||
_isRefreshingMicList = false;
|
||||
}
|
||||
}
|
||||
|
||||
void fetchRoomTaskClaimableCount() {
|
||||
@ -577,6 +786,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future exitCurrentVoiceRoomSession(bool isLogout) async {
|
||||
_setExitingCurrentVoiceRoomSession(true);
|
||||
_stopRoomStatePolling();
|
||||
try {
|
||||
rtmProvider?.msgAllListener = null;
|
||||
rtmProvider?.msgChatListener = null;
|
||||
@ -613,6 +824,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
Future<void> resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async {
|
||||
rtmProvider ??= fallbackRtmProvider;
|
||||
_stopRoomStatePolling();
|
||||
try {
|
||||
SCHeartbeatUtils.cancelTimer();
|
||||
SCHeartbeatUtils.cancelAnchorTimer();
|
||||
@ -635,12 +847,14 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
///清空列表数据
|
||||
void _clearData() {
|
||||
_stopRoomStatePolling();
|
||||
roomRocketStatus = null;
|
||||
rtmProvider
|
||||
?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] =
|
||||
null;
|
||||
roomWheatMap.clear();
|
||||
onlineUsers.clear();
|
||||
managerUsers.clear();
|
||||
needUpDataUserInfo = false;
|
||||
SCRoomUtils.roomUsersMap.clear();
|
||||
roomIsMute = false;
|
||||
@ -649,6 +863,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
redPacketList.clear();
|
||||
currenRoom = null;
|
||||
_roomVisualEffectsEnabled = false;
|
||||
_isExitingCurrentVoiceRoomSession = false;
|
||||
isMic = true;
|
||||
isMusicPlaying = false;
|
||||
DataPersistence.setLastTimeRoomId("");
|
||||
@ -745,6 +960,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
if (seatUser.id == currentUserId) {
|
||||
if (isRoomAdmin) {
|
||||
return false;
|
||||
}
|
||||
_openRoomUserInfoCard(seatUser.id);
|
||||
return true;
|
||||
}
|
||||
@ -757,6 +975,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
void _refreshMicListSilently() {
|
||||
retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {});
|
||||
}
|
||||
|
||||
void _openRoomUserInfoCard(String? userId) {
|
||||
final normalizedUserId = (userId ?? '').trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
@ -813,35 +1035,35 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
SCHeartbeatUtils.scheduleAnchorHeartbeat(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
);
|
||||
var us = roomWheatMap[index];
|
||||
us?.copyWith(
|
||||
user: SocialChatUserProfile(
|
||||
id: myUser?.id,
|
||||
account: myUser?.account,
|
||||
userAvatar: myUser?.userAvatar,
|
||||
userNickname: myUser?.userNickname,
|
||||
userSex: myUser?.userSex,
|
||||
),
|
||||
final currentSeat = roomWheatMap[index];
|
||||
if (currentSeat != null && myUser != null) {
|
||||
roomWheatMap[index] = currentSeat.copyWith(
|
||||
user: myUser.copyWith(roles: currenRoom?.entrants?.roles),
|
||||
micMute: micGoUpRes.micMute,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
);
|
||||
roomWheatMap[index] = us!;
|
||||
if (us.micMute!) {
|
||||
}
|
||||
if (roomWheatMap[index]?.micMute ?? false) {
|
||||
///房主上麦自动解禁麦位
|
||||
if (isFz()) {
|
||||
jieJinMai(index);
|
||||
await jieJinMai(index);
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
_refreshMicListSilently();
|
||||
} catch (ex) {
|
||||
SCTts.show('Failed to put on the microphone, $ex');
|
||||
}
|
||||
}
|
||||
|
||||
xiaMai(num index) async {
|
||||
SCChatRoomRepository()
|
||||
.micGoDown(currenRoom?.roomProfile?.roomProfile?.id ?? "", index)
|
||||
.whenComplete(() {
|
||||
//自己
|
||||
try {
|
||||
await SCChatRoomRepository().micGoDown(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
index,
|
||||
);
|
||||
|
||||
if (roomWheatMap[index]?.user?.id ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
isMic = true;
|
||||
@ -852,9 +1074,15 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
/// 设置成主持人角色
|
||||
engine?.renewToken("");
|
||||
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
roomWheatMap[index]?.setUser = null;
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
roomWheatMap.clear();
|
||||
for (var mic in mics) {
|
||||
roomWheatMap[mic.micIndex!] = mic;
|
||||
if (mic.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
if (mic.micMute!) {
|
||||
///麦克风静音
|
||||
engine?.muteLocalAudioStream(true);
|
||||
} else {
|
||||
// if (!isMic) {
|
||||
// engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
|
||||
// }
|
||||
// engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
|
||||
///这个用户需要禁麦/解禁麦克风
|
||||
engine?.muteLocalAudioStream(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
|
||||
///判断自己是不是在麦上
|
||||
num index = userOnMaiInIndex(
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
||||
_applyMicSnapshot(_buildMicMap(mics));
|
||||
final isOnMic =
|
||||
userOnMaiInIndex(AccountStorage().getCurrentUser()?.userProfile?.id ?? "") >
|
||||
-1;
|
||||
SCHeartbeatUtils.scheduleHeartbeat(
|
||||
SCHeartbeatStatus.VOICE_LIVE.name,
|
||||
isOnMic,
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
);
|
||||
if (index == -1) {
|
||||
engine?.muteLocalAudioStream(true);
|
||||
}
|
||||
}
|
||||
|
||||
///找一个空麦位 -1表示没有空位
|
||||
@ -1044,25 +1255,37 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
///解锁麦位
|
||||
void jieFeng(num index) async {
|
||||
Future<void> jieFeng(num index) async {
|
||||
await SCChatRoomRepository().micLock(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
index,
|
||||
false,
|
||||
);
|
||||
final mic = roomWheatMap[index];
|
||||
if (mic != null) {
|
||||
roomWheatMap[index] = mic.copyWith(micLock: false);
|
||||
notifyListeners();
|
||||
}
|
||||
_refreshMicListSilently();
|
||||
}
|
||||
|
||||
///锁麦
|
||||
void fengMai(num index) async {
|
||||
Future<void> fengMai(num index) async {
|
||||
await SCChatRoomRepository().micLock(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
index,
|
||||
true,
|
||||
);
|
||||
final mic = roomWheatMap[index];
|
||||
if (mic != null) {
|
||||
roomWheatMap[index] = mic.copyWith(micLock: true);
|
||||
notifyListeners();
|
||||
}
|
||||
_refreshMicListSilently();
|
||||
}
|
||||
|
||||
///静音麦克风
|
||||
void jinMai(num index) async {
|
||||
Future<void> jinMai(num index) async {
|
||||
await SCChatRoomRepository().micMute(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
index,
|
||||
@ -1073,16 +1296,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
context!,
|
||||
listen: false,
|
||||
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
Provider.of<RtcProvider>(
|
||||
context!,
|
||||
listen: false,
|
||||
).engine?.muteLocalAudioStream(true);
|
||||
}
|
||||
var mic = roomWheatMap[index];
|
||||
if (mic != null) {
|
||||
roomWheatMap[index] = mic.copyWith(micMute: true);
|
||||
notifyListeners();
|
||||
}
|
||||
_refreshMicListSilently();
|
||||
}
|
||||
|
||||
///解除静音麦克风
|
||||
void jieJinMai(num index) async {
|
||||
Future<void> jieJinMai(num index) async {
|
||||
await SCChatRoomRepository().micMute(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
index,
|
||||
@ -1109,6 +1337,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
roomWheatMap[index] = mic.copyWith(micMute: false);
|
||||
notifyListeners();
|
||||
}
|
||||
_refreshMicListSilently();
|
||||
}
|
||||
|
||||
void addOnlineUser(String groupId, SocialChatUserProfile user) {
|
||||
@ -1124,6 +1353,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
if (!isExtOnlineList) {
|
||||
Provider.of<RtcProvider>(context!, listen: false).onlineUsers.add(user);
|
||||
_refreshManagerUsers(onlineUsers);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -1144,6 +1374,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
context!,
|
||||
listen: false,
|
||||
).onlineUsers.remove(isExtOnlineUser);
|
||||
_refreshManagerUsers(onlineUsers);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,7 @@ typedef RtmProvider = RealTimeMessagingManager;
|
||||
|
||||
class RealTimeMessagingManager extends ChangeNotifier {
|
||||
static const int _giftComboMergeWindowMs = 3000;
|
||||
static const int _maxLuckGiftPushQueueLength = 12;
|
||||
|
||||
BuildContext? context;
|
||||
|
||||
@ -257,13 +258,26 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
TencentImSDKPlugin.v2TIMManager.addGroupListener(
|
||||
listener: V2TimGroupListener(
|
||||
onMemberEnter:
|
||||
(String groupID, List<V2TimGroupMemberInfo> memberList) {},
|
||||
onMemberLeave: (String groupID, V2TimGroupMemberInfo member) {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
onMemberEnter: (String groupID, List<V2TimGroupMemberInfo> memberList) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).removOnlineUser(groupID, member.userID!);
|
||||
);
|
||||
if (groupID ==
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) {
|
||||
rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false);
|
||||
}
|
||||
},
|
||||
onMemberLeave: (String groupID, V2TimGroupMemberInfo member) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
rtcProvider.removOnlineUser(groupID, member.userID!);
|
||||
if (groupID ==
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) {
|
||||
rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false);
|
||||
}
|
||||
},
|
||||
onMemberKicked: (
|
||||
String groupID,
|
||||
@ -275,13 +289,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (memberList.first.userID ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
context,
|
||||
listen: false,
|
||||
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
|
||||
///退出房间
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
context,
|
||||
listen: false,
|
||||
).exitCurrentVoiceRoomSession(false).whenComplete(() {
|
||||
SCRoomUtils.closeAllDialogs();
|
||||
@ -1314,7 +1328,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
listen: false,
|
||||
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
}
|
||||
// Provider.of<RealTimeCommunicationManager>(context!, listen: false).getMicList();
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).retrieveMicrophoneList(notifyIfUnchanged: false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1353,11 +1370,19 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
context!,
|
||||
listen: false,
|
||||
).micChange(BroadCastMicChangePush.fromJson(data).data?.mics);
|
||||
} else if (msg.type == SCRoomMsgType.shangMai ||
|
||||
msg.type == SCRoomMsgType.xiaMai ||
|
||||
msg.type == SCRoomMsgType.fengMai ||
|
||||
msg.type == SCRoomMsgType.jieFeng) {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).retrieveMicrophoneList(notifyIfUnchanged: false);
|
||||
} else if (msg.type == SCRoomMsgType.refreshOnlineUser) {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).fetchOnlineUsersList();
|
||||
).fetchOnlineUsersList(notifyIfUnchanged: false);
|
||||
} else if (msg.type == SCRoomMsgType.gameLuckyGift) {
|
||||
var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data);
|
||||
_giftFxLog(
|
||||
@ -1669,6 +1694,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
|
||||
void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) {
|
||||
if (SCGlobalConfig.isLuckGiftSpecialEffects) {
|
||||
while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) {
|
||||
_luckGiftPushQueue.removeFirst();
|
||||
}
|
||||
_luckGiftPushQueue.add(broadCastRes);
|
||||
playLuckGiftBackCoins();
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
|
||||
|
||||
class GiftAnimationManager extends ChangeNotifier {
|
||||
static const int _maxPendingAnimations = 24;
|
||||
|
||||
Queue<LGiftModel> pendingAnimationsQueue = Queue<LGiftModel>();
|
||||
List<LGiftScrollingScreenAnimsBean> animationControllerList = [];
|
||||
|
||||
@ -15,10 +17,77 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
animationControllerList.length >= giftMap.length;
|
||||
|
||||
void enqueueGiftAnimation(LGiftModel giftModel) {
|
||||
if (_mergeIntoActiveAnimation(giftModel)) {
|
||||
return;
|
||||
}
|
||||
if (_mergeIntoPendingAnimation(giftModel)) {
|
||||
return;
|
||||
}
|
||||
_trimPendingAnimations();
|
||||
pendingAnimationsQueue.add(giftModel);
|
||||
proceedToNextAnimation();
|
||||
}
|
||||
|
||||
void _trimPendingAnimations() {
|
||||
while (pendingAnimationsQueue.length >= _maxPendingAnimations) {
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
bool _mergeIntoActiveAnimation(LGiftModel incoming) {
|
||||
for (final entry in giftMap.entries) {
|
||||
final current = entry.value;
|
||||
if (current == null || current.labelId != incoming.labelId) {
|
||||
continue;
|
||||
}
|
||||
_mergeGiftModel(target: current, incoming: incoming);
|
||||
notifyListeners();
|
||||
if (animationControllerList.length > entry.key) {
|
||||
animationControllerList[entry.key].controller.forward(from: 0.45);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _mergeIntoPendingAnimation(LGiftModel incoming) {
|
||||
for (final pending in pendingAnimationsQueue) {
|
||||
if (pending.labelId != incoming.labelId) {
|
||||
continue;
|
||||
}
|
||||
_mergeGiftModel(target: pending, incoming: incoming);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _mergeGiftModel({
|
||||
required LGiftModel target,
|
||||
required LGiftModel incoming,
|
||||
}) {
|
||||
target.giftCount = (target.giftCount) + (incoming.giftCount);
|
||||
if (target.sendUserName.isEmpty) {
|
||||
target.sendUserName = incoming.sendUserName;
|
||||
}
|
||||
if (target.sendToUserName.isEmpty) {
|
||||
target.sendToUserName = incoming.sendToUserName;
|
||||
}
|
||||
if (target.sendUserPic.isEmpty) {
|
||||
target.sendUserPic = incoming.sendUserPic;
|
||||
}
|
||||
if (target.giftPic.isEmpty) {
|
||||
target.giftPic = incoming.giftPic;
|
||||
}
|
||||
if (target.giftName.isEmpty) {
|
||||
target.giftName = incoming.giftName;
|
||||
}
|
||||
if (incoming.rewardAmountText.isNotEmpty) {
|
||||
target.rewardAmountText = incoming.rewardAmountText;
|
||||
}
|
||||
target.showLuckyRewardFrame =
|
||||
target.showLuckyRewardFrame || incoming.showLuckyRewardFrame;
|
||||
}
|
||||
|
||||
///开始播放
|
||||
proceedToNextAnimation() {
|
||||
if (pendingAnimationsQueue.isEmpty || !_controllersReady) {
|
||||
@ -35,28 +104,7 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
break;
|
||||
} else {
|
||||
if (value.labelId == playGift.labelId) {
|
||||
playGift.giftCount = value.giftCount + playGift.giftCount;
|
||||
if (playGift.sendUserName.isEmpty) {
|
||||
playGift.sendUserName = value.sendUserName;
|
||||
}
|
||||
if (playGift.sendToUserName.isEmpty) {
|
||||
playGift.sendToUserName = value.sendToUserName;
|
||||
}
|
||||
if (playGift.sendUserPic.isEmpty) {
|
||||
playGift.sendUserPic = value.sendUserPic;
|
||||
}
|
||||
if (playGift.giftPic.isEmpty) {
|
||||
playGift.giftPic = value.giftPic;
|
||||
}
|
||||
if (playGift.giftName.isEmpty) {
|
||||
playGift.giftName = value.giftName;
|
||||
}
|
||||
if (playGift.rewardAmountText.isEmpty) {
|
||||
playGift.rewardAmountText = value.rewardAmountText;
|
||||
}
|
||||
playGift.showLuckyRewardFrame =
|
||||
playGift.showLuckyRewardFrame || value.showLuckyRewardFrame;
|
||||
giftMap[key] = playGift;
|
||||
_mergeGiftModel(target: value, incoming: playGift);
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
notifyListeners();
|
||||
animationControllerList[key].controller.forward(from: 0.45);
|
||||
@ -80,6 +128,7 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
for (var element in animationControllerList) {
|
||||
element.controller.dispose();
|
||||
}
|
||||
animationControllerList.clear();
|
||||
}
|
||||
|
||||
void markAnimationAsFinished(int index) {
|
||||
|
||||
451
lib/services/payment/mifa_pay_manager.dart
Normal file
451
lib/services/payment/mifa_pay_manager.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -72,6 +72,7 @@ class RoomProfile {
|
||||
String? userId,
|
||||
SocialChatUserProfile? userProfile,
|
||||
String? userSVipLevel,
|
||||
RoomMemberCounter? roomCounter,
|
||||
ExtValues? extValues,
|
||||
}) {
|
||||
_countryCode = countryCode;
|
||||
@ -91,6 +92,7 @@ class RoomProfile {
|
||||
_userId = userId;
|
||||
_userProfile = userProfile;
|
||||
_userSVipLevel = userSVipLevel;
|
||||
_roomCounter = roomCounter;
|
||||
_extValues = extValues;
|
||||
}
|
||||
|
||||
@ -118,6 +120,9 @@ class RoomProfile {
|
||||
? SocialChatUserProfile.fromJson(json['userProfile'])
|
||||
: null;
|
||||
_userSVipLevel = json['userSVipLevel'];
|
||||
final counterJson = json['roomCounter'] ?? json['counter'];
|
||||
_roomCounter =
|
||||
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
|
||||
_extValues =
|
||||
json['extValues'] != null
|
||||
? ExtValues.fromJson(json['extValues'])
|
||||
@ -140,6 +145,7 @@ class RoomProfile {
|
||||
String? _userId;
|
||||
SocialChatUserProfile? _userProfile;
|
||||
String? _userSVipLevel;
|
||||
RoomMemberCounter? _roomCounter;
|
||||
ExtValues? _extValues;
|
||||
RoomProfile copyWith({
|
||||
String? countryCode,
|
||||
@ -159,6 +165,7 @@ class RoomProfile {
|
||||
String? userId,
|
||||
SocialChatUserProfile? userProfile,
|
||||
String? userSVipLevel,
|
||||
RoomMemberCounter? roomCounter,
|
||||
ExtValues? extValues,
|
||||
}) => RoomProfile(
|
||||
countryCode: countryCode ?? _countryCode,
|
||||
@ -178,6 +185,7 @@ class RoomProfile {
|
||||
userId: userId ?? _userId,
|
||||
userProfile: userProfile ?? _userProfile,
|
||||
userSVipLevel: userSVipLevel ?? _userSVipLevel,
|
||||
roomCounter: roomCounter ?? _roomCounter,
|
||||
extValues: extValues ?? _extValues,
|
||||
);
|
||||
String? get countryCode => _countryCode;
|
||||
@ -197,8 +205,24 @@ class RoomProfile {
|
||||
String? get userId => _userId;
|
||||
SocialChatUserProfile? get userProfile => _userProfile;
|
||||
String? get userSVipLevel => _userSVipLevel;
|
||||
RoomMemberCounter? get roomCounter => _roomCounter;
|
||||
ExtValues? get extValues => _extValues;
|
||||
|
||||
String get displayMemberCount {
|
||||
final memberCount = roomCounter?.memberCount;
|
||||
if (memberCount != null) {
|
||||
if (memberCount == memberCount.roundToDouble()) {
|
||||
return memberCount.toInt().toString();
|
||||
}
|
||||
return memberCount.toString();
|
||||
}
|
||||
final memberQuantity = extValues?.memberQuantity;
|
||||
if ((memberQuantity ?? "").trim().isNotEmpty) {
|
||||
return memberQuantity!;
|
||||
}
|
||||
return "0";
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['countryCode'] = _countryCode;
|
||||
@ -220,6 +244,9 @@ class RoomProfile {
|
||||
map['userProfile'] = _userProfile?.toJson();
|
||||
}
|
||||
map['userSVipLevel'] = _userSVipLevel;
|
||||
if (_roomCounter != null) {
|
||||
map['roomCounter'] = _roomCounter?.toJson();
|
||||
}
|
||||
if (_extValues != null) {
|
||||
map['extValues'] = _extValues?.toJson();
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ class SocialChatRoomRes {
|
||||
String? userId,
|
||||
SocialChatUserProfile? userProfile,
|
||||
String? userSVipLevel,
|
||||
RoomMemberCounter? roomCounter,
|
||||
ExtValues? extValues,
|
||||
}) {
|
||||
_countryCode = countryCode;
|
||||
@ -56,6 +57,7 @@ class SocialChatRoomRes {
|
||||
_userId = userId;
|
||||
_userProfile = userProfile;
|
||||
_userSVipLevel = userSVipLevel;
|
||||
_roomCounter = roomCounter;
|
||||
_extValues = extValues;
|
||||
}
|
||||
|
||||
@ -85,6 +87,9 @@ class SocialChatRoomRes {
|
||||
? SocialChatUserProfile.fromJson(json['userProfile'])
|
||||
: null;
|
||||
_userSVipLevel = json['userSVipLevel'];
|
||||
final counterJson = json['roomCounter'] ?? json['counter'];
|
||||
_roomCounter =
|
||||
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
|
||||
_extValues =
|
||||
json['extValues'] != null
|
||||
? ExtValues.fromJson(json['extValues'])
|
||||
@ -108,6 +113,7 @@ class SocialChatRoomRes {
|
||||
String? _userId;
|
||||
SocialChatUserProfile? _userProfile;
|
||||
String? _userSVipLevel;
|
||||
RoomMemberCounter? _roomCounter;
|
||||
ExtValues? _extValues;
|
||||
|
||||
SocialChatRoomRes copyWith({
|
||||
@ -128,6 +134,7 @@ class SocialChatRoomRes {
|
||||
String? userId,
|
||||
SocialChatUserProfile? userProfile,
|
||||
String? userSVipLevel,
|
||||
RoomMemberCounter? roomCounter,
|
||||
ExtValues? extValues,
|
||||
}) => SocialChatRoomRes(
|
||||
countryCode: countryCode ?? _countryCode,
|
||||
@ -147,6 +154,7 @@ class SocialChatRoomRes {
|
||||
userId: userId ?? _userId,
|
||||
userProfile: userProfile ?? _userProfile,
|
||||
userSVipLevel: userSVipLevel ?? _userSVipLevel,
|
||||
roomCounter: roomCounter ?? _roomCounter,
|
||||
extValues: extValues ?? _extValues,
|
||||
);
|
||||
|
||||
@ -184,8 +192,22 @@ class SocialChatRoomRes {
|
||||
|
||||
String? get userSVipLevel => _userSVipLevel;
|
||||
|
||||
RoomMemberCounter? get roomCounter => _roomCounter;
|
||||
|
||||
ExtValues? get extValues => _extValues;
|
||||
|
||||
String get displayMemberCount {
|
||||
final memberCount = roomCounter?.memberCount;
|
||||
if (memberCount != null) {
|
||||
return _formatRoomMemberCount(memberCount);
|
||||
}
|
||||
final memberQuantity = extValues?.memberQuantity;
|
||||
if ((memberQuantity ?? "").trim().isNotEmpty) {
|
||||
return memberQuantity!;
|
||||
}
|
||||
return "0";
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['countryCode'] = _countryCode;
|
||||
@ -207,6 +229,9 @@ class SocialChatRoomRes {
|
||||
map['userProfile'] = _userProfile?.toJson();
|
||||
}
|
||||
map['userSVipLevel'] = _userSVipLevel;
|
||||
if (_roomCounter != null) {
|
||||
map['roomCounter'] = _roomCounter?.toJson();
|
||||
}
|
||||
if (_extValues != null) {
|
||||
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 : ""
|
||||
/// accountStatus : ""
|
||||
/// age : 0
|
||||
|
||||
317
lib/shared/business_logic/models/res/sc_mifa_pay_res.dart
Normal file
317
lib/shared/business_logic/models/res/sc_mifa_pay_res.dart
Normal 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());
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
|
||||
import 'package:yumi/shared/business_logic/models/res/country_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_level_config_res.dart';
|
||||
@ -26,15 +27,17 @@ abstract class SocialChatConfigRepository {
|
||||
///最近活动通知列表.
|
||||
Future<CountryRes> getNoticeMessage();
|
||||
|
||||
///最近活动排行
|
||||
Future<List<SCTopFourWithRewardRes>> topFourWithReward();
|
||||
|
||||
|
||||
|
||||
///获得SudCode
|
||||
Future<CountryRes> getSudCode();
|
||||
|
||||
///获得客服.
|
||||
Future<CountryRes> getCustomerService();
|
||||
|
||||
///获取平台banner.
|
||||
Future<List<SCIndexBannerRes>> getBanner({List<String>?types});
|
||||
Future<List<SCIndexBannerRes>> getBanner({List<String>? types});
|
||||
|
||||
///获取商品配置
|
||||
Future<List<SCProductConfigRes>> productConfig();
|
||||
@ -55,11 +58,28 @@ abstract class SocialChatConfigRepository {
|
||||
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();
|
||||
|
||||
|
||||
|
||||
///获取APP最新版本
|
||||
Future<SCVersionManageLatestRes> versionManageLatest();
|
||||
|
||||
@ -68,5 +88,4 @@ abstract class SocialChatConfigRepository {
|
||||
|
||||
///获得客服
|
||||
Future<SocialChatUserProfile> customerService();
|
||||
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ class SCFloatingMessage {
|
||||
num? number;
|
||||
num? multiple;
|
||||
int priority = 10; //排序权重
|
||||
int aggregateVersion = 0;
|
||||
|
||||
SCFloatingMessage({
|
||||
this.type = 0,
|
||||
@ -29,6 +30,7 @@ class SCFloatingMessage {
|
||||
this.coins = 0,
|
||||
this.priority = 10,
|
||||
this.multiple = 10,
|
||||
this.aggregateVersion = 0,
|
||||
});
|
||||
|
||||
SCFloatingMessage.fromJson(dynamic json) {
|
||||
@ -46,6 +48,7 @@ class SCFloatingMessage {
|
||||
number = json['number'];
|
||||
priority = json['priority'];
|
||||
multiple = json['multiple'];
|
||||
aggregateVersion = json['aggregateVersion'] ?? 0;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@ -64,6 +67,7 @@ class SCFloatingMessage {
|
||||
map['number'] = number;
|
||||
map['priority'] = priority;
|
||||
map['multiple'] = multiple;
|
||||
map['aggregateVersion'] = aggregateVersion;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,12 +20,12 @@ class OverlayManager {
|
||||
);
|
||||
bool _isPlaying = false;
|
||||
OverlayEntry? _currentOverlayEntry;
|
||||
SCFloatingMessage? _currentMessage;
|
||||
|
||||
bool _isProcessing = false;
|
||||
bool _isDisposed = false;
|
||||
|
||||
static final OverlayManager _instance =
|
||||
OverlayManager._internal();
|
||||
static final OverlayManager _instance = OverlayManager._internal();
|
||||
|
||||
factory OverlayManager() => _instance;
|
||||
|
||||
@ -34,6 +34,9 @@ class OverlayManager {
|
||||
void addMessage(SCFloatingMessage message) {
|
||||
if (_isDisposed) return;
|
||||
if (SCGlobalConfig.isFloatingAnimationInGlobal) {
|
||||
if (_tryAggregateMessage(message)) {
|
||||
return;
|
||||
}
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
} else {
|
||||
@ -44,6 +47,74 @@ class OverlayManager {
|
||||
}
|
||||
}
|
||||
|
||||
bool _tryAggregateMessage(SCFloatingMessage incoming) {
|
||||
if (!_supportsAggregation(incoming)) {
|
||||
return false;
|
||||
}
|
||||
if (_currentMessage != null &&
|
||||
_isSameAggregatedFloatingMessage(_currentMessage!, incoming)) {
|
||||
_mergeFloatingMessage(_currentMessage!, incoming);
|
||||
_currentOverlayEntry?.markNeedsBuild();
|
||||
return true;
|
||||
}
|
||||
for (final queuedMessage in _messageQueue.unorderedElements) {
|
||||
if (_isSameAggregatedFloatingMessage(queuedMessage, incoming)) {
|
||||
_mergeFloatingMessage(queuedMessage, incoming);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _supportsAggregation(SCFloatingMessage message) {
|
||||
return message.type == 0;
|
||||
}
|
||||
|
||||
bool _isSameAggregatedFloatingMessage(
|
||||
SCFloatingMessage existing,
|
||||
SCFloatingMessage incoming,
|
||||
) {
|
||||
return existing.type == incoming.type &&
|
||||
existing.roomId == incoming.roomId &&
|
||||
existing.userId == incoming.userId &&
|
||||
existing.toUserId == incoming.toUserId &&
|
||||
existing.giftUrl == incoming.giftUrl;
|
||||
}
|
||||
|
||||
void _mergeFloatingMessage(
|
||||
SCFloatingMessage target,
|
||||
SCFloatingMessage incoming,
|
||||
) {
|
||||
target.coins = (target.coins ?? 0) + (incoming.coins ?? 0);
|
||||
target.number = (target.number ?? 0) + (incoming.number ?? 0);
|
||||
final currentMultiple = target.multiple ?? 0;
|
||||
final incomingMultiple = incoming.multiple ?? 0;
|
||||
target.multiple =
|
||||
currentMultiple >= incomingMultiple
|
||||
? currentMultiple
|
||||
: incomingMultiple;
|
||||
if ((target.userAvatarUrl ?? '').isEmpty) {
|
||||
target.userAvatarUrl = incoming.userAvatarUrl;
|
||||
}
|
||||
if ((target.userName ?? '').isEmpty) {
|
||||
target.userName = incoming.userName;
|
||||
}
|
||||
if ((target.toUserName ?? '').isEmpty) {
|
||||
target.toUserName = incoming.toUserName;
|
||||
}
|
||||
if ((target.giftUrl ?? '').isEmpty) {
|
||||
target.giftUrl = incoming.giftUrl;
|
||||
}
|
||||
if ((target.roomId ?? '').isEmpty) {
|
||||
target.roomId = incoming.roomId;
|
||||
}
|
||||
target.priority =
|
||||
target.priority >= incoming.priority
|
||||
? target.priority
|
||||
: incoming.priority;
|
||||
target.aggregateVersion += 1;
|
||||
}
|
||||
|
||||
void _safeScheduleNext() {
|
||||
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return;
|
||||
_isProcessing = true;
|
||||
@ -67,7 +138,10 @@ class OverlayManager {
|
||||
final messageToProcess = _messageQueue.first;
|
||||
|
||||
if (messageToProcess?.type == 1) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(context, listen: false);
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (rtcProvider.currenRoom == null) {
|
||||
// 从队列中移除第一个元素
|
||||
_messageQueue.removeFirst();
|
||||
@ -88,9 +162,11 @@ class OverlayManager {
|
||||
|
||||
void _playMessage(SCFloatingMessage message) {
|
||||
_isPlaying = true;
|
||||
_currentMessage = message;
|
||||
final context = navigatorKey.currentState?.context;
|
||||
if (context == null || !context.mounted) {
|
||||
_isPlaying = false;
|
||||
_currentMessage = null;
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
@ -106,7 +182,7 @@ class OverlayManager {
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context)?.insert(_currentOverlayEntry!);
|
||||
Overlay.of(context).insert(_currentOverlayEntry!);
|
||||
}
|
||||
|
||||
Widget _buildScreenWidget(SCFloatingMessage message) {
|
||||
@ -119,10 +195,12 @@ class OverlayManager {
|
||||
try {
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_isPlaying = false;
|
||||
_safeScheduleNext();
|
||||
} catch (e) {
|
||||
debugPrint('清理悬浮消息出错: $e');
|
||||
_currentMessage = null;
|
||||
_isPlaying = false;
|
||||
_safeScheduleNext();
|
||||
}
|
||||
@ -131,6 +209,9 @@ class OverlayManager {
|
||||
switch (message.type) {
|
||||
case 0:
|
||||
return FloatingLuckGiftScreenWidget(
|
||||
key: ValueKey<String>(
|
||||
'luck_${message.userId}_${message.toUserId}_${message.giftUrl}_${message.aggregateVersion}',
|
||||
),
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
@ -172,6 +253,7 @@ class OverlayManager {
|
||||
_isDisposed = true;
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_messageQueue.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
|
||||
@ -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_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_version_manage_latest_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/country_res.dart';
|
||||
@ -32,7 +33,7 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
|
||||
///sys/config/banner
|
||||
@override
|
||||
Future<List<SCIndexBannerRes>> getBanner({List<String>?types}) async {
|
||||
Future<List<SCIndexBannerRes>> getBanner({List<String>? types}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
if (types != null) {
|
||||
params["types"] = types;
|
||||
@ -67,8 +68,6 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///ranking/top-four-with-reward
|
||||
@override
|
||||
Future<List<SCTopFourWithRewardRes>> topFourWithReward() async {
|
||||
@ -94,7 +93,6 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
///sys/config/banner/start-page
|
||||
@override
|
||||
Future<List<SCStartPageRes>> getStartPage() async {
|
||||
@ -134,7 +132,9 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
"1585c72b88d0d249c7078aae852a0806cc3d8e483b8d861962a977a00523180a",
|
||||
fromJson:
|
||||
(json) =>
|
||||
(json as List).map((e) => SCProductConfigRes.fromJson(e)).toList(),
|
||||
(json as List)
|
||||
.map((e) => SCProductConfigRes.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
@ -185,6 +185,63 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
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
|
||||
@override
|
||||
Future<SCLevelConfigRes> configLevel() async {
|
||||
@ -195,10 +252,9 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
///sys/version/manage/release/latest
|
||||
@override
|
||||
Future<SCVersionManageLatestRes> versionManageLatest() async{
|
||||
Future<SCVersionManageLatestRes> versionManageLatest() async {
|
||||
final result = await http.get<SCVersionManageLatestRes>(
|
||||
"ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687",
|
||||
fromJson: (json) => SCVersionManageLatestRes.fromJson(json),
|
||||
@ -208,7 +264,7 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
|
||||
///sys/version/manage/latest/review
|
||||
@override
|
||||
Future<VersionManageLatesReviewRes> versionManageLatestReview() async{
|
||||
Future<VersionManageLatesReviewRes> versionManageLatestReview() async {
|
||||
final result = await http.get<VersionManageLatesReviewRes>(
|
||||
"ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232",
|
||||
fromJson: (json) => VersionManageLatesReviewRes.fromJson(json),
|
||||
@ -218,12 +274,11 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
|
||||
///sys/config/customer-service
|
||||
@override
|
||||
Future<SocialChatUserProfile> customerService() async{
|
||||
Future<SocialChatUserProfile> customerService() async {
|
||||
final result = await http.get<SocialChatUserProfile>(
|
||||
"ba316258c14cc3ebddb6d28ec314bc5704e593861ca693058e9e98ab3114cf05",
|
||||
fromJson: (json) => SocialChatUserProfile.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -13,9 +13,13 @@ import 'package:tancent_vap/widgets/vap_view.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
|
||||
class SCGiftVapSvgaManager {
|
||||
static const int _maxPendingTaskCount = 18;
|
||||
Map<String, MovieEntity> videoItemCache = {};
|
||||
static SCGiftVapSvgaManager? _inst;
|
||||
static const int _maxPreloadConcurrency = 1;
|
||||
static const int _maxPreloadQueueLength = 12;
|
||||
static const int _maxSvgaCacheEntries = 8;
|
||||
static const int _maxPlayablePathCacheEntries = 24;
|
||||
|
||||
SCGiftVapSvgaManager._internal();
|
||||
|
||||
@ -86,7 +90,10 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
|
||||
Future<void> preload(String path, {bool highPriority = false}) async {
|
||||
if (path.isEmpty || _dis || _isPreloadedOrLoading(path)) {
|
||||
if (!SCGlobalConfig.allowsHighCostAnimations ||
|
||||
path.isEmpty ||
|
||||
_dis ||
|
||||
_isPreloadedOrLoading(path)) {
|
||||
return;
|
||||
}
|
||||
if (highPriority) {
|
||||
@ -97,12 +104,20 @@ class SCGiftVapSvgaManager {
|
||||
if (_queuedPreloadPaths.contains(path)) {
|
||||
return;
|
||||
}
|
||||
_trimPreloadQueue();
|
||||
_preloadQueue.add(path);
|
||||
_queuedPreloadPaths.add(path);
|
||||
_log('enqueue preload path=$path queue=${_preloadQueue.length}');
|
||||
_drainPreloadQueue();
|
||||
}
|
||||
|
||||
void _trimPreloadQueue() {
|
||||
while (_preloadQueue.length >= _maxPreloadQueueLength) {
|
||||
final removedPath = _preloadQueue.removeFirst();
|
||||
_queuedPreloadPaths.remove(removedPath);
|
||||
}
|
||||
}
|
||||
|
||||
void _drainPreloadQueue() {
|
||||
if (_dis ||
|
||||
_pause ||
|
||||
@ -137,7 +152,7 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
|
||||
Future<MovieEntity> _loadSvgaEntity(String path) async {
|
||||
final cached = videoItemCache[path];
|
||||
final cached = _touchCachedSvgaEntity(path);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
@ -159,7 +174,9 @@ class SCGiftVapSvgaManager {
|
||||
throw Exception('Unsupported SVGA path: $path');
|
||||
}
|
||||
entity.autorelease = false;
|
||||
videoItemCache[path] = entity;
|
||||
if (!_dis) {
|
||||
_cacheSvgaEntity(path, entity);
|
||||
}
|
||||
return entity;
|
||||
}();
|
||||
_svgaLoadTasks[path] = future;
|
||||
@ -175,7 +192,7 @@ class SCGiftVapSvgaManager {
|
||||
if (pathType == PathType.asset || pathType == PathType.file) {
|
||||
return path;
|
||||
}
|
||||
final cachedPath = _playablePathCache[path];
|
||||
final cachedPath = _touchCachedPlayablePath(path);
|
||||
if (cachedPath != null &&
|
||||
cachedPath.isNotEmpty &&
|
||||
File(cachedPath).existsSync()) {
|
||||
@ -187,7 +204,9 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
final future = () async {
|
||||
final file = await FileCacheManager.getInstance().getFile(url: path);
|
||||
_playablePathCache[path] = file.path;
|
||||
if (!_dis) {
|
||||
_cachePlayablePath(path, file.path);
|
||||
}
|
||||
return file.path;
|
||||
}();
|
||||
_playablePathTasks[path] = future;
|
||||
@ -198,6 +217,41 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
}
|
||||
|
||||
MovieEntity? _touchCachedSvgaEntity(String path) {
|
||||
final cached = videoItemCache.remove(path);
|
||||
if (cached != null) {
|
||||
videoItemCache[path] = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
void _cacheSvgaEntity(String path, MovieEntity entity) {
|
||||
videoItemCache.remove(path);
|
||||
videoItemCache[path] = entity;
|
||||
_trimResolvedCache(videoItemCache, _maxSvgaCacheEntries);
|
||||
}
|
||||
|
||||
String? _touchCachedPlayablePath(String path) {
|
||||
final cachedPath = _playablePathCache.remove(path);
|
||||
if (cachedPath != null) {
|
||||
_playablePathCache[path] = cachedPath;
|
||||
}
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
void _cachePlayablePath(String path, String playablePath) {
|
||||
_playablePathCache.remove(path);
|
||||
_playablePathCache[path] = playablePath;
|
||||
_trimResolvedCache(_playablePathCache, _maxPlayablePathCacheEntries);
|
||||
}
|
||||
|
||||
void _trimResolvedCache<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}) {
|
||||
if (_dis) {
|
||||
return;
|
||||
@ -301,7 +355,21 @@ class SCGiftVapSvgaManager {
|
||||
customResources: customResources,
|
||||
);
|
||||
|
||||
if (_tq.length >= _maxPendingTaskCount && priority <= 0) {
|
||||
_log(
|
||||
'drop play request because queue is full path=$path '
|
||||
'priority=$priority queue=${_tq.length}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_tq.add(task);
|
||||
while (_tq.length > _maxPendingTaskCount) {
|
||||
final removedTask = _tq.removeLast();
|
||||
_log(
|
||||
'trim queued task path=${removedTask.path} '
|
||||
'priority=${removedTask.priority} queue=${_tq.length}',
|
||||
);
|
||||
}
|
||||
_log('task enqueued path=$path queueAfter=${_tq.length}');
|
||||
if (!_play) {
|
||||
_pn();
|
||||
@ -486,6 +554,10 @@ class SCGiftVapSvgaManager {
|
||||
_log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}');
|
||||
_dis = true;
|
||||
stopPlayback();
|
||||
_svgaLoadTasks.clear();
|
||||
_playablePathTasks.clear();
|
||||
videoItemCache.clear();
|
||||
_playablePathCache.clear();
|
||||
_rgc?.dispose();
|
||||
_rgc = null;
|
||||
_rsc?.dispose();
|
||||
@ -564,6 +636,11 @@ class SCPriorityQueue<E> {
|
||||
|
||||
void clear() => _els.clear();
|
||||
|
||||
E removeLast() {
|
||||
if (isEmpty) throw StateError("No elements");
|
||||
return _els.removeLast();
|
||||
}
|
||||
|
||||
List<E> get unorderedElements => List.from(_els);
|
||||
|
||||
// 实现 Iterable 接口
|
||||
|
||||
@ -115,11 +115,17 @@ class SCPageListState<M, T extends SCPageList> extends State<SCPageList> {
|
||||
},
|
||||
child:
|
||||
needLoading && isLoading
|
||||
? Center(child: CupertinoActivityIndicator(color: Colors.white24,))
|
||||
? Center(
|
||||
child: CupertinoActivityIndicator(
|
||||
color: Colors.white24,
|
||||
),
|
||||
)
|
||||
: empty(),
|
||||
)
|
||||
: needLoading && isLoading
|
||||
? Center(child: CupertinoActivityIndicator(color: Colors.white24))
|
||||
? Center(
|
||||
child: CupertinoActivityIndicator(color: Colors.white24),
|
||||
)
|
||||
: empty())
|
||||
: _buildList(),
|
||||
),
|
||||
@ -210,4 +216,10 @@ class SCPageListState<M, T extends SCPageList> extends State<SCPageList> {
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,8 @@ class RoomAnimationQueueScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
static const int _maxAnimationQueueLength = 12;
|
||||
|
||||
final List<AnimationTask> _animationQueue = [];
|
||||
bool _isQueueProcessing = false;
|
||||
final Map<int, GlobalKey<_RoomEntranceAnimationState>> _animationKeys = {};
|
||||
@ -46,6 +48,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
final taskId = DateTime.now().millisecondsSinceEpoch;
|
||||
final animationKey = GlobalKey<_RoomEntranceAnimationState>();
|
||||
|
||||
_trimQueueOverflow();
|
||||
_animationKeys[taskId] = animationKey;
|
||||
|
||||
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}) {
|
||||
if (_animationQueue.isEmpty) return;
|
||||
|
||||
@ -94,7 +106,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
||||
});
|
||||
} else {
|
||||
// 重试多次失败后,跳过当前动画
|
||||
print("动画启动失败,跳过当前任务");
|
||||
debugPrint("动画启动失败,跳过当前任务");
|
||||
task.onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,8 @@ class RoomGiftSeatFlightRequest {
|
||||
}
|
||||
|
||||
class RoomGiftSeatFlightController {
|
||||
static const int _maxBufferedRequests = 24;
|
||||
|
||||
static final RoomGiftSeatFlightController _instance =
|
||||
RoomGiftSeatFlightController._internal();
|
||||
|
||||
@ -46,6 +48,7 @@ class RoomGiftSeatFlightController {
|
||||
}
|
||||
|
||||
if (_state == null) {
|
||||
_trimPendingRequestsOverflow();
|
||||
_pendingRequests.add(normalizedRequest);
|
||||
return;
|
||||
}
|
||||
@ -69,6 +72,7 @@ class RoomGiftSeatFlightController {
|
||||
|
||||
if (_state == null) {
|
||||
_trimPendingRequestsForTag(queueTag, maxTrackedRequests);
|
||||
_trimPendingRequestsOverflow();
|
||||
_pendingRequests.add(normalizedRequest);
|
||||
return;
|
||||
}
|
||||
@ -168,6 +172,12 @@ class RoomGiftSeatFlightController {
|
||||
}
|
||||
}
|
||||
|
||||
void _trimPendingRequestsOverflow() {
|
||||
while (_pendingRequests.length >= _maxBufferedRequests) {
|
||||
_pendingRequests.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
void _attach(_RoomGiftSeatFlightOverlayState state) {
|
||||
_state = state;
|
||||
while (_pendingRequests.isNotEmpty) {
|
||||
@ -199,6 +209,8 @@ class RoomGiftSeatFlightOverlay extends StatefulWidget {
|
||||
|
||||
class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const int _maxQueuedRequests = 24;
|
||||
|
||||
final Queue<_QueuedRoomGiftSeatFlightRequest> _queue = Queue();
|
||||
final GlobalKey _overlayKey = GlobalKey();
|
||||
|
||||
@ -240,6 +252,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
|
||||
void _enqueue(RoomGiftSeatFlightRequest request) {
|
||||
_ensureCenterVisual(request);
|
||||
_trimQueuedRequestsOverflow();
|
||||
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
|
||||
_scheduleNextAnimation();
|
||||
}
|
||||
@ -346,6 +359,12 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
return false;
|
||||
}
|
||||
|
||||
void _trimQueuedRequestsOverflow() {
|
||||
while (_queue.length >= _maxQueuedRequests) {
|
||||
_queue.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleNextAnimation() {
|
||||
if (_isPlaying || _queue.isEmpty || !mounted) {
|
||||
return;
|
||||
|
||||
@ -303,6 +303,9 @@ class _FloatingLuckGiftScreenWidgetState
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
strutStyle: StrutStyle(
|
||||
height: 1.1,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
||||
@ -5,25 +5,37 @@ import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
|
||||
import '../../../../modules/room/seat/sc_seat_item.dart';
|
||||
|
||||
|
||||
|
||||
class RoomSeatWidget extends StatefulWidget {
|
||||
@override
|
||||
_RoomSeatWidgetState createState() => _RoomSeatWidgetState();
|
||||
}
|
||||
|
||||
class _RoomSeatWidgetState extends State<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
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<RtcProvider>(
|
||||
builder: (context, ref, child) {
|
||||
return ref.roomWheatMap.length == 5
|
||||
final int seatCount = _resolvedSeatCount(ref);
|
||||
return seatCount == 5
|
||||
? _buildSeat5()
|
||||
: (ref.roomWheatMap.length == 10
|
||||
: (seatCount == 10
|
||||
? _buildSeat10()
|
||||
: (ref.roomWheatMap.length == 15
|
||||
: (seatCount == 15
|
||||
? _buildSeat15()
|
||||
: (ref.roomWheatMap.length == 20
|
||||
: (seatCount == 20
|
||||
? _buildSeat20()
|
||||
: Container(height: 180.w))));
|
||||
},
|
||||
|
||||
@ -25,6 +25,12 @@ class _RoomMicSwitchPageState extends State<RoomMicSwitchPage>
|
||||
_tabController = TabController(length: _pages.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_tabs.clear();
|
||||
|
||||
BIN
sc_images/index/sc_icon_home_room_rank_border_1.svga
Normal file
BIN
sc_images/index/sc_icon_home_room_rank_border_1.svga
Normal file
Binary file not shown.
BIN
sc_images/index/sc_icon_home_room_rank_border_2.svga
Normal file
BIN
sc_images/index/sc_icon_home_room_rank_border_2.svga
Normal file
Binary file not shown.
BIN
sc_images/index/sc_icon_home_room_rank_border_3.svga
Normal file
BIN
sc_images/index/sc_icon_home_room_rank_border_3.svga
Normal file
Binary file not shown.
BIN
sc_images/room/sc_icon_room_chat_tab_all_selected.png
Normal file
BIN
sc_images/room/sc_icon_room_chat_tab_all_selected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
sc_images/room/sc_icon_room_chat_tab_all_unselected.png
Normal file
BIN
sc_images/room/sc_icon_room_chat_tab_all_unselected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
sc_images/room/sc_icon_room_chat_tab_chat_selected.png
Normal file
BIN
sc_images/room/sc_icon_room_chat_tab_chat_selected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
sc_images/room/sc_icon_room_chat_tab_chat_unselected.png
Normal file
BIN
sc_images/room/sc_icon_room_chat_tab_chat_unselected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
sc_images/room/sc_icon_room_chat_tab_gift_selected.png
Normal file
BIN
sc_images/room/sc_icon_room_chat_tab_gift_selected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
sc_images/room/sc_icon_room_chat_tab_gift_unselected.png
Normal file
BIN
sc_images/room/sc_icon_room_chat_tab_gift_unselected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
16
需求进度.md
16
需求进度.md
@ -20,6 +20,9 @@
|
||||
- 本轮按需求暂未处理网络链路上的启动等待,例如审核态检查或远端启动页配置请求。
|
||||
|
||||
## 已完成模块
|
||||
- 已按 2026-04-20 最新首页视觉需求,为 Party 房间列表前 3 个房卡接入新的本地排名边框 SVGA:桌面“房间排序前三的框”中的 3 份素材已导入工程并挂到首页房卡最上层,仅作用于当前列表前 3 项,其余房卡保持原样;后续又继续为前三房卡底部信息区补了横向与底部安全区,避免外扩边框直接压住国旗、房名和在线人数。同时 `Me` 板块里的 `Recent / Followed` tab 已继续对齐上方首页 tab 的斜体字样式,并移除了点击时的波浪反馈。
|
||||
- 已按 2026-04-20 最新房间联调继续修正房主在自己房间里的麦位操作回归:当前房主/管理员点击自己所在麦位时,不再被“直接打开资料卡”逻辑短路,会重新回到底部麦位菜单,从而正常看到自己可用的上下麦/禁麦操作;同时上麦、下麦、禁麦、解禁、锁麦、解锁这些动作在接口成功后会立即回写本地麦位状态,并主动补拉一次最新麦位列表,减少房主端自己操作后 UI 状态延迟或短暂错乱。
|
||||
- 已按 2026-04-20 最新确认继续完成 `Wallet -> Recharge` 的 MiFaPay 真链路接入:页面仍保持“原页面结构 + 局部新增”的方案,只在原充值页白色内容区顶部新增 `Recharge methods`,且该标题已改成项目常见的土豪金色;`Google Pay / Apple Pay` 继续保留原生内购列表,`MiFaPay` 已从占位切成真实接口驱动,进入页面会拉 `/order/web/pay/country`、`/order/web/pay/commodity`,底部弹窗会展示真实商品与支付渠道,确认后调用 `/order/web/pay/recharge` 下单并拉起 MiFaPay H5 WebView。用户关闭收银台返回 App 后,当前会立即刷新钱包余额并轮询最新金币额;命中到账后会直接提示购买成功,避免只停留在“支付确认中”。
|
||||
- 已按 2026-04-18 联调要求继续收口幸运礼物链路:项目默认 API host 已临时切到 `http://192.168.110.43:1100/` 方便直连测试环境;`/gift/give/lucky-gift` 请求体也已补齐为最新结构,除原有 `giftId/quantity/roomId/acceptUserIds/checkCombo` 外,会稳定携带 `gameId: null`、`accepts: []`、`dynamicContentId: null`、`songId: null` 这几组字段,便于和当前后端参数定义保持一致。
|
||||
- 已继续补齐 `GAME_LUCKY_GIFT` 的 socket 播报与中奖动效:房间群消息收到该类型后,现在会统一落聊天室中奖消息、奖励弹层队列与发送者余额回写,不再只在 `3x+` 时才触发顶部奖励动画;同时全局飘窗已改为按服务端 `globalNews` 字段决定是否展示,避免再用前端本地倍率硬编码去猜。
|
||||
- 已按最新 UI 口径重排幸运礼物中奖展示:顶部 `LuckGiftNomorAnimWidget` 不再叠加大头像、倍率和奖励框,只在“单个礼物倍率 `> 10x`”或“单次中奖金币 `> 5000`”时负责播全屏 `luck_gift_reward_burst.svga`;原本的 `luck_gift_reward_frame.svga` 已改挂到房间礼物播报条最右侧,并在框内显示 `+formattedAwardAmount`,金额文案直接复用此前大头像下方那一份中奖金币额。
|
||||
@ -27,6 +30,8 @@
|
||||
- 已继续修正幸运礼物中奖播报条右侧奖励框不显示的问题:根因是 `pubspec.yaml` 之前只显式收录了 `sc_images/room/anim/gift/`,但没有把新的 `sc_images/room/anim/luck_gift/` 子目录单独打进 Flutter 资源清单,导致 `luck_gift_reward_frame.svga` 在运行时可引用但未被实际打包;当前已补齐资源目录声明,并为播报条奖励框增加本地 fallback,避免资源异常时再次只剩金额裸字。
|
||||
- 已按最新文案口径继续收紧幸运礼物中奖消息:聊天室里高倍率幸运礼物提示不再显示冗长的 `Coins / a lucky(magic) gift` 文字,当前已改为直接展示“中奖金额 + 金币图标 + 对应礼物图”,和房间礼物播报条的视觉表达保持一致。
|
||||
- 已继续微调幸运礼物中奖视觉:房间礼物播报条右侧的 `luck_gift_reward_frame.svga` 已进一步放大,并给右侧区域和主条正文预留了更宽的排版空间,避免资源已经正常显示但因为画布比例和透明边距看起来过小;同时聊天室高亮中奖消息与房间顶部幸运礼物横幅现已统一改成“中奖金额 + 金币图标 + from + 礼物图”的表达,去掉原先 `Coins / a lucky(magic) gift` 这类冗长英文文案;另外全屏 `luck_gift_reward_burst.svga` 也已补上中部金额文案,避免播发时只见特效不见本次实际中奖金币数。
|
||||
- 已按 2026-04-20 最新联调继续收口幸运礼物播报噪音:顶部幸运礼物飘屏现在会对同一房间、同一送礼人、同一接收人、同一礼物的连续中奖做队列内聚合,不再每来一条都重新追加一个新飘屏;当前展示中的那一条也会直接叠加金币额并刷新,避免“刷礼过快时飘屏一条接一条排队刷过去”。同时飘屏中奖文案已强制收为单行显示,防止金额、`from` 和礼物图被挤成上下两行。
|
||||
- 已继续修复房间礼物播报条偶发出现两条一模一样内容的问题:此前 `GiftAnimationManager` 只会和“正在播放”的同 `labelId` 项做合并,等待队列里的同类播报不会提前去重,因此高频情况下仍可能排出两条完全一样的条目;当前已改为在入队时同时检查“正在播”和“待播”两侧,命中相同播报键时直接原地合并,不再把重复项继续塞进队列。
|
||||
- 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。
|
||||
- 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。
|
||||
- 已继续微调语言房个人卡片与送礼 UI:个人卡片底部动作文案现已支持两行居中展示,避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。
|
||||
@ -157,6 +162,14 @@
|
||||
|
||||
## 已改动文件
|
||||
- `需求进度.md`
|
||||
- `lib/main.dart`
|
||||
- `lib/modules/wallet/recharge/recharge_page.dart`
|
||||
- `lib/modules/wallet/recharge/recharge_method_bottom_sheet.dart`
|
||||
- `lib/modules/wallet/recharge/mifa_pay_webview_page.dart`
|
||||
- `lib/services/payment/mifa_pay_manager.dart`
|
||||
- `lib/shared/business_logic/models/res/sc_mifa_pay_res.dart`
|
||||
- `lib/shared/business_logic/repositories/config_repository.dart`
|
||||
- `lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart`
|
||||
- `lib/shared/data_sources/models/enum/sc_gift_type.dart`
|
||||
- `lib/shared/tools/sc_network_image_utils.dart`
|
||||
- `lib/shared/tools/sc_gift_vap_svga_manager.dart`
|
||||
@ -193,6 +206,9 @@
|
||||
- `lib/modules/home/popular/mine/sc_home_mine_page.dart`
|
||||
- `lib/ui_kit/widgets/room/room_live_audio_indicator.dart`
|
||||
- `lib/modules/search/sc_search_page.dart`
|
||||
- `sc_images/index/sc_icon_home_room_rank_border_1.svga`
|
||||
- `sc_images/index/sc_icon_home_room_rank_border_2.svga`
|
||||
- `sc_images/index/sc_icon_home_room_rank_border_3.svga`
|
||||
- `lib/modules/store/headdress/store_headdress_page.dart`
|
||||
- `lib/modules/store/mountains/store_mountains_page.dart`
|
||||
- `lib/modules/store/theme/store_theme_page.dart`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user