接口异常处理
This commit is contained in:
parent
b4c45e6400
commit
4d86236227
@ -346,15 +346,15 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 585674be3c6661708e6cd794ab4f39fb9d8356f9
|
||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
|
||||
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
||||
audioplayers_darwin: f15e209a3e856d1a7edcf98dc029f484fead2242
|
||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
||||
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
|
||||
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
|
||||
firebase_auth: 5342db41af2ba5ed32a6177d9e326eecbebda912
|
||||
firebase_core: 99a37263b3c27536063a7b601d9e2a49400a433c
|
||||
firebase_crashlytics: 986773c0c7ea4c3f5a4ead141b58522fcd9dfdb1
|
||||
firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b
|
||||
firebase_core: 995454a784ff288be5689b796deb9e9fa3601818
|
||||
firebase_crashlytics: 30dcd6dfd2fe895c0848af46722a4227346c19aa
|
||||
FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df
|
||||
FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b
|
||||
FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd
|
||||
@ -366,47 +366,47 @@ SPEC CHECKSUMS:
|
||||
FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa
|
||||
FirebaseSessions: b9a92c1c51bbb81e78fc3142cda6d925d700f8e7
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
|
||||
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||
google_sign_in_ios: 7411fab6948df90490dc4620ecbcabdc3ca04017
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063
|
||||
image_cropper: a3291c624a953049bc6a02e1f8c8ceb162a24b25
|
||||
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
|
||||
in_app_purchase_storekit: 2342c0a5da86593124d08dd13d920f39a52b273a
|
||||
image_cropper: 655b3ba703c9e15e3111e79151624d6154288774
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
||||
libpag: 98742fad4b3ac2a2ee31e383d2483495fb9a5fb4
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
on_audio_query_ios: e5afb0d4b42cfa3e8762d8ec1953540cb4f02100
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
pag: ff30ba5e4be59380a5ddac6b183905729be15a3c
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
on_audio_query_ios: 28a780e2d0d85d92d500ba6e12c6c8167022b2fa
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
pag: f01aa9017ab0e04a83ba4a20d6070a50f0ac9da8
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
|
||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82
|
||||
tancent_vap: eceaaf095167d102727cf799c75891057834c311
|
||||
tencent_cloud_chat_sdk: 546bb906bbe585814e3d2d33afdb91f09c0eac44
|
||||
tencent_rtc_sdk: 1ca5201f7bddc691a3a95486325ad6dc792a273d
|
||||
tancent_vap: fa8ad93814a9f950514a7074c662d2c937084c68
|
||||
tencent_cloud_chat_sdk: 55e5fffe20f6b7937a26a674ccccb639563a9790
|
||||
tencent_rtc_sdk: f77558b6b436a149d378557c2a837f73c09061bc
|
||||
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
|
||||
TXCustomBeautyProcesserPlugin: 099393b941cb40eda12b3a80bf6c0319957b1cfd
|
||||
TXIMSDK_Plus_iOS_XCFramework: 5d1933192fb3b7ef2fe933f1623de4a0486a7fe2
|
||||
TXLiteAVSDK_Professional: e792e6084a208ecef6196c5f28237ad8078b4d7c
|
||||
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
||||
video_player_avfoundation: e54d03b3b19f64f2c557354f72ce538387541ba6
|
||||
video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1
|
||||
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
|
||||
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52
|
||||
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
||||
|
||||
PODFILE CHECKSUM: a6f49a93e5f85201a2efdadcd7cf184b0e310894
|
||||
|
||||
|
||||
113
lib/debug/room_cp_relation_formed_effect_preview.dart
Normal file
113
lib/debug/room_cp_relation_formed_effect_preview.dart
Normal file
@ -0,0 +1,113 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_relation_formed_effect.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const RoomCpRelationFormedEffectPreviewApp());
|
||||
}
|
||||
|
||||
class RoomCpRelationFormedEffectPreviewApp extends StatelessWidget {
|
||||
const RoomCpRelationFormedEffectPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: Size(SCScreen.designWidth, SCScreen.designHeight),
|
||||
splitScreenMode: false,
|
||||
minTextAdapt: true,
|
||||
builder: (context, child) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: FlutterSmartDialog.init(),
|
||||
home: const RoomCpRelationFormedEffectPreviewPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomCpRelationFormedEffectPreviewPage extends StatefulWidget {
|
||||
const RoomCpRelationFormedEffectPreviewPage({super.key});
|
||||
|
||||
@override
|
||||
State<RoomCpRelationFormedEffectPreviewPage> createState() =>
|
||||
_RoomCpRelationFormedEffectPreviewPageState();
|
||||
}
|
||||
|
||||
class _RoomCpRelationFormedEffectPreviewPageState
|
||||
extends State<RoomCpRelationFormedEffectPreviewPage> {
|
||||
Timer? _loopTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _showEffect());
|
||||
_loopTimer = Timer.periodic(
|
||||
const Duration(seconds: 7),
|
||||
(_) => _showEffect(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_loopTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showEffect() {
|
||||
RoomCpRelationFormedEffect.show(
|
||||
leftName: "Avery",
|
||||
rightName: "Jordan",
|
||||
leftAvatar: "https://i.pravatar.cc/180?img=32",
|
||||
rightAvatar: "https://i.pravatar.cc/180?img=47",
|
||||
relationType: "CP",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF170D24),
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF2B163A), Color(0xFF07040D)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 34,
|
||||
child: Row(
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: _showEffect,
|
||||
child: const Text("Replay"),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FilledButton.tonal(
|
||||
onPressed:
|
||||
() => SmartDialog.dismiss(
|
||||
tag: RoomCpRelationFormedEffect.dialogTag,
|
||||
),
|
||||
child: const Text("Dismiss"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1164,6 +1164,8 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
}
|
||||
|
||||
class _MessageItem extends StatelessWidget {
|
||||
static bool _isOpeningCpInviteDialog = false;
|
||||
|
||||
final V2TimMessage message;
|
||||
final V2TimMessage? preMessage;
|
||||
final SCVipResourceRes? selfVipChatBubble;
|
||||
@ -1473,15 +1475,30 @@ class _MessageItem extends StatelessWidget {
|
||||
SCSystemInvitMessageRes? cpInvite,
|
||||
RoomCpInviteActionState actionState,
|
||||
) async {
|
||||
final latestState = await _latestCpInviteStateForMessage(data);
|
||||
if (!context.mounted) {
|
||||
if (_isOpeningCpInviteDialog ||
|
||||
SmartDialog.checkExist(tag: RoomCpInviteDialog.dialogTag)) {
|
||||
return;
|
||||
}
|
||||
final resolvedActionState =
|
||||
latestState == _CpInviteMessageState.pending
|
||||
? actionState
|
||||
: _cpInviteActionState(latestState);
|
||||
_showCpInviteDialogFromMessage(data, cpInvite, resolvedActionState);
|
||||
_isOpeningCpInviteDialog = true;
|
||||
try {
|
||||
_showCpInviteDialogFromMessage(data, cpInvite, actionState);
|
||||
if (actionState != RoomCpInviteActionState.pending) {
|
||||
return;
|
||||
}
|
||||
final latestState = await _latestCpInviteStateForMessage(data);
|
||||
if (!context.mounted ||
|
||||
latestState == _CpInviteMessageState.pending ||
|
||||
!SmartDialog.checkExist(tag: RoomCpInviteDialog.dialogTag)) {
|
||||
return;
|
||||
}
|
||||
_showCpInviteDialogFromMessage(
|
||||
data,
|
||||
cpInvite,
|
||||
_cpInviteActionState(latestState),
|
||||
);
|
||||
} finally {
|
||||
_isOpeningCpInviteDialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCpInviteGiftThumb(String giftCover) {
|
||||
|
||||
@ -275,6 +275,8 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
|
||||
}
|
||||
|
||||
class _MessageItem extends StatelessWidget {
|
||||
static bool _isOpeningCpInviteDialog = false;
|
||||
|
||||
final V2TimMessage message;
|
||||
final V2TimMessage? preMessage;
|
||||
List<V2TimMessage> currentConversationMessageList = [];
|
||||
@ -546,15 +548,30 @@ class _MessageItem extends StatelessWidget {
|
||||
SCSystemInvitMessageRes? cpInvite,
|
||||
RoomCpInviteActionState actionState,
|
||||
) async {
|
||||
final latestState = await _latestCpInviteStateForMessage(data);
|
||||
if (!context.mounted) {
|
||||
if (_isOpeningCpInviteDialog ||
|
||||
SmartDialog.checkExist(tag: RoomCpInviteDialog.dialogTag)) {
|
||||
return;
|
||||
}
|
||||
final resolvedActionState =
|
||||
latestState == _CpInviteMessageState.pending
|
||||
? actionState
|
||||
: _cpInviteActionState(latestState);
|
||||
_showCpInviteDialogFromMessage(data, cpInvite, resolvedActionState);
|
||||
_isOpeningCpInviteDialog = true;
|
||||
try {
|
||||
_showCpInviteDialogFromMessage(data, cpInvite, actionState);
|
||||
if (actionState != RoomCpInviteActionState.pending) {
|
||||
return;
|
||||
}
|
||||
final latestState = await _latestCpInviteStateForMessage(data);
|
||||
if (!context.mounted ||
|
||||
latestState == _CpInviteMessageState.pending ||
|
||||
!SmartDialog.checkExist(tag: RoomCpInviteDialog.dialogTag)) {
|
||||
return;
|
||||
}
|
||||
_showCpInviteDialogFromMessage(
|
||||
data,
|
||||
cpInvite,
|
||||
_cpInviteActionState(latestState),
|
||||
);
|
||||
} finally {
|
||||
_isOpeningCpInviteDialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCpInviteGiftThumb(String giftCover) {
|
||||
|
||||
@ -1871,7 +1871,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
toUserAvatarUrl: u.user?.userAvatar ?? "",
|
||||
giftUrl: gift.giftPhoto ?? "",
|
||||
giftId: gift.id,
|
||||
giftTab: gift.giftTab,
|
||||
giftTab: _floatingGiftTabFor(gift),
|
||||
roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
coins: coins,
|
||||
number: quantity,
|
||||
@ -1955,7 +1955,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
toUserName: receiver?.userNickname ?? "",
|
||||
giftUrl: gift.giftPhoto ?? "",
|
||||
giftId: gift.id,
|
||||
giftTab: gift.giftTab,
|
||||
giftTab: _floatingGiftTabFor(gift),
|
||||
number: quantity,
|
||||
coins: coins,
|
||||
roomId: roomId,
|
||||
@ -2001,6 +2001,13 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
].join("|");
|
||||
}
|
||||
|
||||
String _floatingGiftTabFor(SocialChatGiftRes gift) {
|
||||
if (_isCpGift(gift)) {
|
||||
return scCpRelationTypeForGift(gift);
|
||||
}
|
||||
return gift.giftTab ?? "";
|
||||
}
|
||||
|
||||
bool _supportsComboFeedback(SocialChatGiftRes gift) {
|
||||
final giftTab = (gift.giftTab ?? '').trim();
|
||||
return giftTab == "LUCK" ||
|
||||
|
||||
@ -2046,15 +2046,21 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
|
||||
return Consumer<SocialChatUserProfileManager>(
|
||||
builder: (context, ref, child) {
|
||||
final bool isProfileLoading = ref.userProfile == null;
|
||||
final bool profileLoadFailed =
|
||||
ref.userProfileLoadError != null &&
|
||||
!ref.isUserProfileLoading &&
|
||||
ref.userProfile == null;
|
||||
List<PersonPhoto> backgroundPhotos = [];
|
||||
backgroundPhotos =
|
||||
(isProfileLoading
|
||||
(isProfileLoading || profileLoadFailed
|
||||
? <PersonPhoto>[]
|
||||
: (ref.userProfile?.backgroundPhotos ?? []))
|
||||
.where((t) => t.status == 1)
|
||||
.toList();
|
||||
final dataCard =
|
||||
isProfileLoading ? null : _activeDataCard(ref.userProfile);
|
||||
isProfileLoading || profileLoadFailed
|
||||
? null
|
||||
: _activeDataCard(ref.userProfile);
|
||||
return Directionality(
|
||||
textDirection:
|
||||
window.locale.languageCode == "ar"
|
||||
@ -2389,25 +2395,32 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
|
||||
body:
|
||||
isBlacklist
|
||||
? Container()
|
||||
: (isProfileLoading || isBlacklistLoading
|
||||
? const SizedBox.shrink()
|
||||
: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProfileHeader(
|
||||
ref,
|
||||
backgroundPhotos,
|
||||
dataCard,
|
||||
: (profileLoadFailed
|
||||
? _buildProfileLoadFailed(ref)
|
||||
: (isProfileLoading || isBlacklistLoading
|
||||
? const SizedBox.shrink()
|
||||
: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProfileHeader(
|
||||
ref,
|
||||
backgroundPhotos,
|
||||
dataCard,
|
||||
),
|
||||
_buildProfileColumnContent(
|
||||
ref.userProfile,
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildProfileColumnContent(ref.userProfile),
|
||||
],
|
||||
),
|
||||
)),
|
||||
))),
|
||||
),
|
||||
// 底部按钮区域
|
||||
isBlacklistLoading || isBlacklist || isProfileLoading
|
||||
isBlacklistLoading ||
|
||||
isBlacklist ||
|
||||
isProfileLoading ||
|
||||
profileLoadFailed
|
||||
? Container()
|
||||
: Positioned(
|
||||
bottom: 0,
|
||||
@ -2428,6 +2441,33 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileLoadFailed(SocialChatUserProfileManager ref) {
|
||||
final localizations = SCAppLocalizations.of(context);
|
||||
return Center(
|
||||
child: SCDebounceWidget(
|
||||
onTap: () => ref.getUserInfoById(widget.tageId),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.refresh, color: Colors.white70, size: 32.w),
|
||||
SizedBox(height: 12.w),
|
||||
text(
|
||||
localizations?.loadingFailedClickToRetry ??
|
||||
'Loading failed, click to retry',
|
||||
textAlign: TextAlign.center,
|
||||
textColor: Colors.white70,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPartWaysDialog(SocialChatUserProfileManager ref) {
|
||||
SmartDialog.show(
|
||||
tag: "showPartWaysDialog",
|
||||
|
||||
@ -4020,8 +4020,22 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
|
||||
String _resolveRegionGiftTab(Map<String, dynamic> payload) {
|
||||
final payloadGiftTab = _payloadText(payload['giftTab']);
|
||||
final payloadRelationType = _firstNonBlank([
|
||||
_payloadText(payload['relationType']),
|
||||
_payloadText(payload['cpRelationType']),
|
||||
]);
|
||||
if (payloadGiftTab.isNotEmpty) {
|
||||
return payloadGiftTab;
|
||||
final gift = SocialChatGiftRes(
|
||||
giftTab: payloadGiftTab,
|
||||
relationType: payloadRelationType,
|
||||
);
|
||||
return _floatingGiftTabFor(gift);
|
||||
}
|
||||
if (payloadRelationType.isNotEmpty) {
|
||||
final gift = SocialChatGiftRes(relationType: payloadRelationType);
|
||||
if (scIsCpGift(gift)) {
|
||||
return _floatingGiftTabFor(gift);
|
||||
}
|
||||
}
|
||||
final giftId = _payloadText(payload['giftId']);
|
||||
final currentContext = context;
|
||||
@ -4032,7 +4046,19 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
currentContext,
|
||||
listen: false,
|
||||
);
|
||||
return appGeneralManager.getGiftByIdOrStandardId(giftId)?.giftTab ?? '';
|
||||
return _floatingGiftTabFor(
|
||||
appGeneralManager.getGiftByIdOrStandardId(giftId),
|
||||
);
|
||||
}
|
||||
|
||||
String _floatingGiftTabFor(SocialChatGiftRes? gift) {
|
||||
if (gift == null) {
|
||||
return '';
|
||||
}
|
||||
if (scIsCpGift(gift)) {
|
||||
return scCpRelationTypeForGift(gift);
|
||||
}
|
||||
return gift.giftTab ?? '';
|
||||
}
|
||||
|
||||
bool _isCurrentVoiceRoom(String roomId) {
|
||||
@ -6338,7 +6364,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
toUserAvatarUrl: msg.toUser?.userAvatar ?? "",
|
||||
giftUrl: gift.giftPhoto,
|
||||
giftId: gift.id,
|
||||
giftTab: gift.giftTab,
|
||||
giftTab: _floatingGiftTabFor(gift),
|
||||
number: quantity,
|
||||
coins: coins,
|
||||
roomId: roomId,
|
||||
@ -6400,7 +6426,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
toUserAvatarUrl: msg.toUser?.userAvatar ?? "",
|
||||
giftUrl: gift.giftPhoto,
|
||||
giftId: gift.id,
|
||||
giftTab: gift.giftTab,
|
||||
giftTab: _floatingGiftTabFor(gift),
|
||||
number: msg.number,
|
||||
coins: coins,
|
||||
roomId: msg.msg,
|
||||
|
||||
@ -132,6 +132,8 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
|
||||
/// 当前正在查看的个人详情资料,不用于承载登录用户自己的 Me 页数据。
|
||||
SocialChatUserProfile? userProfile;
|
||||
Object? userProfileLoadError;
|
||||
bool isUserProfileLoading = false;
|
||||
int _userInfoRequestSerial = 0;
|
||||
|
||||
Future<void> getUserInfoById(String userId) {
|
||||
@ -147,30 +149,122 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
required bool clearBeforeLoad,
|
||||
}) async {
|
||||
final requestSerial = ++_userInfoRequestSerial;
|
||||
final normalizedUserId = userId.trim();
|
||||
if (clearBeforeLoad) {
|
||||
userProfile = null;
|
||||
notifyListeners();
|
||||
}
|
||||
final loadedProfile = await SCAccountRepository().loadUserInfo(userId);
|
||||
var mergedProfile = await _withCurrentVipLevel(loadedProfile, userId);
|
||||
mergedProfile = await _withCpCabinProfiles(mergedProfile, userId);
|
||||
if (requestSerial != _userInfoRequestSerial) {
|
||||
userProfileLoadError = null;
|
||||
isUserProfileLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
if (normalizedUserId.isEmpty) {
|
||||
if (requestSerial == _userInfoRequestSerial) {
|
||||
userProfileLoadError = ArgumentError('userId is empty');
|
||||
isUserProfileLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
return;
|
||||
}
|
||||
userProfile = mergedProfile;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final loadedProfile = await SCAccountRepository().loadUserInfo(
|
||||
normalizedUserId,
|
||||
);
|
||||
var mergedProfile = await _withCurrentVipLevel(
|
||||
loadedProfile,
|
||||
normalizedUserId,
|
||||
);
|
||||
mergedProfile = await _withCpCabinProfiles(
|
||||
mergedProfile,
|
||||
normalizedUserId,
|
||||
);
|
||||
if (requestSerial != _userInfoRequestSerial) {
|
||||
return;
|
||||
}
|
||||
userProfile = mergedProfile;
|
||||
userProfileLoadError = null;
|
||||
} catch (error, stackTrace) {
|
||||
if (requestSerial != _userInfoRequestSerial) {
|
||||
return;
|
||||
}
|
||||
userProfile = null;
|
||||
userProfileLoadError = error;
|
||||
_debugProfileLoadFailure(
|
||||
scope: 'detail',
|
||||
userId: normalizedUserId,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
} finally {
|
||||
if (requestSerial == _userInfoRequestSerial) {
|
||||
isUserProfileLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///房间资料卡
|
||||
RoomUserCardRes? userCardInfo;
|
||||
Object? userCardLoadError;
|
||||
bool isUserCardLoading = false;
|
||||
int _userCardRequestSerial = 0;
|
||||
|
||||
// List<UserCountGuardRes>? userCountGuardResList;
|
||||
|
||||
void roomUserCard(String roomId, String userId) async {
|
||||
Future<void> roomUserCard(String roomId, String userId) async {
|
||||
final requestSerial = ++_userCardRequestSerial;
|
||||
final normalizedRoomId = roomId.trim();
|
||||
final normalizedUserId = userId.trim();
|
||||
userCardInfo = null;
|
||||
userCardInfo = await SCChatRoomRepository().roomUserCard(roomId, userId);
|
||||
userCardInfo = await _withCurrentVipLevelForCard(userCardInfo, userId);
|
||||
userCardLoadError = null;
|
||||
isUserCardLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
if (normalizedUserId.isEmpty) {
|
||||
if (requestSerial == _userCardRequestSerial) {
|
||||
userCardLoadError = ArgumentError('userId is empty');
|
||||
isUserCardLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
RoomUserCardRes? card = await SCChatRoomRepository().roomUserCard(
|
||||
normalizedRoomId,
|
||||
normalizedUserId,
|
||||
);
|
||||
card = await _withCurrentVipLevelForCard(card, normalizedUserId);
|
||||
if (requestSerial != _userCardRequestSerial) {
|
||||
return;
|
||||
}
|
||||
userCardInfo = card;
|
||||
userCardLoadError = null;
|
||||
} catch (error, stackTrace) {
|
||||
if (requestSerial != _userCardRequestSerial) {
|
||||
return;
|
||||
}
|
||||
userCardInfo = null;
|
||||
userCardLoadError = error;
|
||||
_debugProfileLoadFailure(
|
||||
scope: 'roomCard',
|
||||
userId: normalizedUserId,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
} finally {
|
||||
if (requestSerial == _userCardRequestSerial) {
|
||||
isUserCardLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clearRoomUserCard() {
|
||||
_userCardRequestSerial++;
|
||||
userCardInfo = null;
|
||||
userCardLoadError = null;
|
||||
isUserCardLoading = false;
|
||||
}
|
||||
|
||||
Future<RoomUserCardRes?> _withCurrentVipLevelForCard(
|
||||
@ -189,6 +283,19 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
return card.copyWith(userProfile: mergedProfile);
|
||||
}
|
||||
|
||||
void _debugProfileLoadFailure({
|
||||
required String scope,
|
||||
required String userId,
|
||||
required Object error,
|
||||
required StackTrace stackTrace,
|
||||
}) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[ProfileLoad][$scope] failed userId=$userId error=$error\n$stackTrace',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<SocialChatUserProfile> _withCpCabinProfiles(
|
||||
SocialChatUserProfile profile,
|
||||
String userId,
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||
import 'package:yumi/shared/tools/sc_entrance_vap_svga_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
@ -579,7 +580,7 @@ class OverlayManager {
|
||||
case 0:
|
||||
return 83;
|
||||
case 1:
|
||||
return 54;
|
||||
return scIsCpGiftTab(message.giftTab) ? 65 : 54;
|
||||
case 3:
|
||||
return 68;
|
||||
case 4:
|
||||
|
||||
@ -253,10 +253,30 @@ class BaseNetworkClient {
|
||||
);
|
||||
|
||||
// 处理基础响应
|
||||
final baseResponse = ResponseData.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
fromJsonT: fromJson,
|
||||
);
|
||||
final responseData = response.data;
|
||||
if (responseData is! Map) {
|
||||
throw DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: '响应格式错误: ${responseData.runtimeType}',
|
||||
);
|
||||
}
|
||||
|
||||
final ResponseData<T> baseResponse;
|
||||
try {
|
||||
baseResponse = ResponseData.fromJson(
|
||||
Map<String, dynamic>.from(responseData),
|
||||
fromJsonT: fromJson,
|
||||
);
|
||||
} catch (error) {
|
||||
throw DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: '响应解析失败: $error',
|
||||
);
|
||||
}
|
||||
// 业务逻辑成功判断
|
||||
if (baseResponse.success) {
|
||||
if (baseResponse.body != null) {
|
||||
|
||||
@ -343,7 +343,36 @@ class ApiInterceptor extends Interceptor {
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) async {
|
||||
if (response.statusCode == 200) {
|
||||
final ResponseData respData = ResponseData.fromJson(response.data);
|
||||
final responseData = response.data;
|
||||
if (responseData is! Map) {
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: '响应格式错误: ${responseData.runtimeType}',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final ResponseData respData;
|
||||
try {
|
||||
respData = ResponseData.fromJson(
|
||||
Map<String, dynamic>.from(responseData),
|
||||
);
|
||||
} catch (error) {
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: '响应解析失败: $error',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (respData.success) {
|
||||
handler.next(response);
|
||||
} else {
|
||||
@ -378,11 +407,13 @@ class ApiInterceptor extends Interceptor {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Fluttertoast.showToast(msg: 'Network error, please check the status.');
|
||||
final message = _httpResponseErrorMessage(response);
|
||||
Fluttertoast.showToast(msg: message);
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
error: 'HTTP ${response.statusCode} 错误',
|
||||
type: DioExceptionType.badResponse,
|
||||
error: message,
|
||||
response: response,
|
||||
),
|
||||
);
|
||||
@ -390,6 +421,25 @@ class ApiInterceptor extends Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
String _httpResponseErrorMessage(Response response) {
|
||||
final statusCode = response.statusCode;
|
||||
final responseData = response.data;
|
||||
if (responseData is Map) {
|
||||
try {
|
||||
final respData = ResponseData.fromJson(
|
||||
Map<String, dynamic>.from(responseData),
|
||||
);
|
||||
final code = respData.errorCode ?? statusCode;
|
||||
final message = respData.errorMsg;
|
||||
if (message != null && message.trim().isNotEmpty) {
|
||||
return 'HTTP $statusCode: $message';
|
||||
}
|
||||
return 'HTTP $statusCode 错误${code == null ? "" : " ($code)"}';
|
||||
} catch (_) {}
|
||||
}
|
||||
return 'HTTP $statusCode 错误';
|
||||
}
|
||||
|
||||
/// 响应数据结构
|
||||
class ResponseData<T> {
|
||||
final bool status;
|
||||
@ -481,12 +531,23 @@ class ResponseData<T> {
|
||||
}
|
||||
}
|
||||
|
||||
final code = json['code']?.toString().trim().toLowerCase();
|
||||
return code == 'success' || code == 'ok';
|
||||
final codeValue = json['code'] ?? json['errorCode'];
|
||||
if (codeValue is num) {
|
||||
return codeValue == 0 || codeValue == 200;
|
||||
}
|
||||
final code = codeValue?.toString().trim().toLowerCase();
|
||||
if (code == null || code.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (code == 'success' || code == 'ok' || code == 'true') {
|
||||
return true;
|
||||
}
|
||||
final numericCode = int.tryParse(code);
|
||||
return numericCode == 0 || numericCode == 200;
|
||||
}
|
||||
|
||||
static int? _readErrorCode(Map<String, dynamic> json) {
|
||||
final errorCode = json['errorCode'];
|
||||
final errorCode = json['errorCode'] ?? json['code'];
|
||||
if (errorCode is int) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart';
|
||||
@ -56,161 +60,303 @@ class RoomCpRelationFormedEffect extends StatefulWidget {
|
||||
_RoomCpRelationFormedEffectState();
|
||||
}
|
||||
|
||||
class _RoomCpRelationFormedEffectState
|
||||
extends State<RoomCpRelationFormedEffect> {
|
||||
class _RoomCpRelationFormedEffectState extends State<RoomCpRelationFormedEffect>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const bool _debugSlots = bool.fromEnvironment(
|
||||
"CP_FORMED_EFFECT_DEBUG",
|
||||
);
|
||||
|
||||
final SCGiftMp4Controller _controller = SCGiftMp4Controller();
|
||||
late final AnimationController _timelineController;
|
||||
late final Future<RoomCpRelationFormedVapLayout?> _layoutFuture;
|
||||
Timer? _fallbackDismissTimer;
|
||||
Timer? _avatarTimer;
|
||||
bool _showAvatars = false;
|
||||
bool _timelineStarted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_timelineController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 5),
|
||||
);
|
||||
_layoutFuture = RoomCpRelationFormedVapLayout.loadFromAsset(
|
||||
RoomCpRelationFormedEffect._assetPath,
|
||||
).then((layout) {
|
||||
if (layout != null && mounted) {
|
||||
_timelineController.duration = layout.duration;
|
||||
if (_debugSlots) {
|
||||
debugPrint(
|
||||
"[RoomCpEffect] vap layout "
|
||||
"canvas=${layout.canvasSize.width}x${layout.canvasSize.height} "
|
||||
"fps=${layout.fps} frames=${layout.totalFrames} "
|
||||
"dynamicFrames=${layout.firstDynamicFrame}-${layout.lastDynamicFrame} "
|
||||
"leftTag=${layout.leftTag} rightTag=${layout.rightTag}",
|
||||
);
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
});
|
||||
_controller.setPlaybackListener(
|
||||
onStart: _startTimeline,
|
||||
onComplete: _dismiss,
|
||||
onFailed: (_) => _dismiss(),
|
||||
);
|
||||
_fallbackDismissTimer = Timer(const Duration(seconds: 7), _dismiss);
|
||||
_avatarTimer = Timer(const Duration(milliseconds: 650), () {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_showAvatars = true;
|
||||
});
|
||||
});
|
||||
unawaited(_controller.playAsset(RoomCpRelationFormedEffect._assetPath));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fallbackDismissTimer?.cancel();
|
||||
_avatarTimer?.cancel();
|
||||
_timelineController.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startTimeline() {
|
||||
_timelineStarted = true;
|
||||
_timelineController
|
||||
..stop()
|
||||
..value = 0
|
||||
..forward();
|
||||
}
|
||||
|
||||
void _dismiss() {
|
||||
_fallbackDismissTimer?.cancel();
|
||||
_timelineController.stop();
|
||||
SmartDialog.dismiss(tag: RoomCpRelationFormedEffect.dialogTag);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mediaSize = MediaQuery.sizeOf(context);
|
||||
final screenWidth = ScreenUtil().screenWidth;
|
||||
final screenHeight = ScreenUtil().screenHeight;
|
||||
final videoWidth = screenWidth;
|
||||
final videoHeight = videoWidth * 1280 / 1088;
|
||||
final videoTop = (screenHeight - videoHeight) / 2;
|
||||
final avatarSize = 55.w;
|
||||
final avatarTop = videoTop + videoHeight * 0.56;
|
||||
final width = mediaSize.width > 0 ? mediaSize.width : screenWidth;
|
||||
final height = mediaSize.height > 0 ? mediaSize.height : screenHeight;
|
||||
|
||||
return IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(child: GiftMp4EffectView(controller: _controller)),
|
||||
Positioned(
|
||||
left: screenWidth / 2 - 112.w,
|
||||
top: avatarTop,
|
||||
child: _EffectAvatar(
|
||||
avatarUrl: widget.leftAvatar,
|
||||
name: widget.leftName,
|
||||
size: avatarSize,
|
||||
visible: _showAvatars,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: screenWidth / 2 + 57.w,
|
||||
top: avatarTop,
|
||||
child: _EffectAvatar(
|
||||
avatarUrl: widget.rightAvatar,
|
||||
name: widget.rightName,
|
||||
size: avatarSize,
|
||||
visible: _showAvatars,
|
||||
),
|
||||
),
|
||||
],
|
||||
width: width,
|
||||
height: height,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final bounds = _resolveBounds(constraints, Size(width, height));
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GiftMp4EffectView(controller: _controller),
|
||||
),
|
||||
FutureBuilder<RoomCpRelationFormedVapLayout?>(
|
||||
future: _layoutFuture,
|
||||
builder: (context, snapshot) {
|
||||
final layout = snapshot.data;
|
||||
if (layout == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return _buildMetadataDrivenAvatars(layout, bounds);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Size _resolveBounds(BoxConstraints constraints, Size fallback) {
|
||||
final width =
|
||||
constraints.hasBoundedWidth && constraints.maxWidth > 0
|
||||
? constraints.maxWidth
|
||||
: fallback.width;
|
||||
final height =
|
||||
constraints.hasBoundedHeight && constraints.maxHeight > 0
|
||||
? constraints.maxHeight
|
||||
: fallback.height;
|
||||
return Size(width, height);
|
||||
}
|
||||
|
||||
Widget _buildMetadataDrivenAvatars(
|
||||
RoomCpRelationFormedVapLayout layout,
|
||||
Size bounds,
|
||||
) {
|
||||
return AnimatedBuilder(
|
||||
animation: _timelineController,
|
||||
builder: (context, _) {
|
||||
final frameIndex =
|
||||
_timelineStarted
|
||||
? layout.frameForProgress(_timelineController.value)
|
||||
: 0;
|
||||
final leftPlacement = layout.leftPlacementForFrame(frameIndex);
|
||||
final rightPlacement = layout.rightPlacementForFrame(frameIndex);
|
||||
final contentRect = layout.contentRectForBounds(bounds);
|
||||
final children = <Widget>[
|
||||
if (leftPlacement != null)
|
||||
..._buildAvatarPlacement(
|
||||
layout.scaledRectForPlacement(leftPlacement, bounds),
|
||||
avatarUrl: widget.leftAvatar,
|
||||
name: widget.leftName,
|
||||
debugLabel: layout.leftTag ?? "left",
|
||||
),
|
||||
if (rightPlacement != null)
|
||||
..._buildAvatarPlacement(
|
||||
layout.scaledRectForPlacement(rightPlacement, bounds),
|
||||
avatarUrl: widget.rightAvatar,
|
||||
name: widget.rightName,
|
||||
debugLabel: layout.rightTag ?? "right",
|
||||
),
|
||||
];
|
||||
|
||||
if (_debugSlots) {
|
||||
children.addAll(
|
||||
_buildDebugOverlay(
|
||||
layout: layout,
|
||||
bounds: bounds,
|
||||
contentRect: contentRect,
|
||||
frameIndex: frameIndex,
|
||||
leftPlacement: leftPlacement,
|
||||
rightPlacement: rightPlacement,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (children.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Stack(clipBehavior: Clip.none, children: children);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildAvatarPlacement(
|
||||
Rect rect, {
|
||||
required String avatarUrl,
|
||||
required String name,
|
||||
required String debugLabel,
|
||||
}) {
|
||||
final labelWidth = math.max(82.w, rect.width + 24.w);
|
||||
return [
|
||||
Positioned.fromRect(
|
||||
rect: rect,
|
||||
child: _EffectAvatar(
|
||||
avatarUrl: avatarUrl,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: rect.center.dx - labelWidth / 2,
|
||||
top: rect.bottom + 5.w,
|
||||
width: labelWidth,
|
||||
child: _EffectName(name: name),
|
||||
),
|
||||
if (_debugSlots)
|
||||
Positioned(
|
||||
left: rect.left,
|
||||
top: rect.top - 14.w,
|
||||
child: _DebugLabel(text: debugLabel),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildDebugOverlay({
|
||||
required RoomCpRelationFormedVapLayout layout,
|
||||
required Size bounds,
|
||||
required Rect contentRect,
|
||||
required int frameIndex,
|
||||
required RoomCpRelationFormedVapPlacement? leftPlacement,
|
||||
required RoomCpRelationFormedVapPlacement? rightPlacement,
|
||||
}) {
|
||||
Rect? scaled(RoomCpRelationFormedVapPlacement? placement) =>
|
||||
placement == null
|
||||
? null
|
||||
: layout.scaledRectForPlacement(placement, bounds);
|
||||
final leftRect = scaled(leftPlacement);
|
||||
final rightRect = scaled(rightPlacement);
|
||||
return [
|
||||
Positioned.fromRect(
|
||||
rect: contentRect,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0x88FF00FF), width: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (leftRect != null)
|
||||
Positioned.fromRect(rect: leftRect, child: _DebugSlotBorder()),
|
||||
if (rightRect != null)
|
||||
Positioned.fromRect(rect: rightRect, child: _DebugSlotBorder()),
|
||||
Positioned(
|
||||
left: contentRect.left + 8.w,
|
||||
top: contentRect.top + 8.w,
|
||||
child: _DebugLabel(
|
||||
text:
|
||||
"frame $frameIndex / ${layout.firstDynamicFrame}-${layout.lastDynamicFrame}",
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _EffectAvatar extends StatelessWidget {
|
||||
const _EffectAvatar({
|
||||
required this.avatarUrl,
|
||||
required this.name,
|
||||
required this.size,
|
||||
required this.visible,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
final String avatarUrl;
|
||||
final String name;
|
||||
final double size;
|
||||
final bool visible;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
opacity: visible ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 240),
|
||||
opacity: 1,
|
||||
duration: const Duration(milliseconds: 120),
|
||||
child: SizedBox(
|
||||
width: 78.w,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: size,
|
||||
height: size,
|
||||
padding: EdgeInsets.all(2.w),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1.5.w),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xffff4f9a).withValues(alpha: 0.45),
|
||||
blurRadius: 12.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipOval(
|
||||
child: head(
|
||||
url: avatarUrl,
|
||||
width: size - 4.w,
|
||||
showDefault: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5.w),
|
||||
Text(
|
||||
_shortName(name),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w700,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
blurRadius: 4.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
width: width,
|
||||
height: height,
|
||||
child: ClipOval(
|
||||
child: head(
|
||||
url: avatarUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
showDefault: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EffectName extends StatelessWidget {
|
||||
const _EffectName({required this.name});
|
||||
|
||||
final String name;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
_shortName(name),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w700,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: [
|
||||
Shadow(color: Colors.black.withValues(alpha: 0.6), blurRadius: 4.w),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _shortName(String value) {
|
||||
final text = value.trim();
|
||||
@ -220,3 +366,398 @@ class _EffectAvatar extends StatelessWidget {
|
||||
return "${text.substring(0, 9)}...";
|
||||
}
|
||||
}
|
||||
|
||||
class _DebugSlotBorder extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFF00F5FF), width: 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DebugLabel extends StatelessWidget {
|
||||
const _DebugLabel({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.56),
|
||||
borderRadius: BorderRadius.circular(3.w),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF00F5FF),
|
||||
fontSize: 8.sp,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class RoomCpRelationFormedVapLayout {
|
||||
const RoomCpRelationFormedVapLayout({
|
||||
required this.canvasSize,
|
||||
required this.fps,
|
||||
required this.totalFrames,
|
||||
required this.placementsByTag,
|
||||
required this.leftTag,
|
||||
required this.rightTag,
|
||||
});
|
||||
|
||||
final Size canvasSize;
|
||||
final double fps;
|
||||
final int totalFrames;
|
||||
final Map<String, List<RoomCpRelationFormedVapPlacement>> placementsByTag;
|
||||
final String? leftTag;
|
||||
final String? rightTag;
|
||||
|
||||
int get firstDynamicFrame {
|
||||
final frames =
|
||||
placementsByTag.values.expand((placements) => placements).toList();
|
||||
if (frames.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
return frames.map((placement) => placement.frameIndex).reduce(math.min);
|
||||
}
|
||||
|
||||
int get lastDynamicFrame {
|
||||
final frames =
|
||||
placementsByTag.values.expand((placements) => placements).toList();
|
||||
if (frames.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
return frames.map((placement) => placement.frameIndex).reduce(math.max);
|
||||
}
|
||||
|
||||
Duration get duration {
|
||||
final seconds = totalFrames > 0 && fps > 0 ? totalFrames / fps : 5.0;
|
||||
return Duration(milliseconds: (seconds * 1000).round());
|
||||
}
|
||||
|
||||
static Future<RoomCpRelationFormedVapLayout?> loadFromAsset(
|
||||
String assetPath,
|
||||
) async {
|
||||
try {
|
||||
final bytes = await rootBundle.load(assetPath);
|
||||
return parseRoomCpRelationFormedVapLayout(bytes.buffer.asUint8List());
|
||||
} catch (error) {
|
||||
debugPrint("[RoomCpEffect] failed to load vap metadata: $error");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int frameForProgress(double progress) {
|
||||
final safeProgress = progress.clamp(0.0, 1.0);
|
||||
final frame = (safeProgress * totalFrames).floor();
|
||||
return frame.clamp(0, math.max(totalFrames - 1, 0));
|
||||
}
|
||||
|
||||
RoomCpRelationFormedVapPlacement? leftPlacementForFrame(int frameIndex) {
|
||||
final tag = leftTag;
|
||||
return tag == null ? null : placementForTag(tag, frameIndex);
|
||||
}
|
||||
|
||||
RoomCpRelationFormedVapPlacement? rightPlacementForFrame(int frameIndex) {
|
||||
final tag = rightTag;
|
||||
return tag == null ? null : placementForTag(tag, frameIndex);
|
||||
}
|
||||
|
||||
RoomCpRelationFormedVapPlacement? placementForTag(
|
||||
String tag,
|
||||
int frameIndex,
|
||||
) {
|
||||
final placements = placementsByTag[tag];
|
||||
if (placements == null || placements.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (frameIndex < placements.first.frameIndex ||
|
||||
frameIndex > placements.last.frameIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RoomCpRelationFormedVapPlacement? lastBefore;
|
||||
for (final placement in placements) {
|
||||
if (placement.frameIndex == frameIndex) {
|
||||
return placement;
|
||||
}
|
||||
if (placement.frameIndex > frameIndex) {
|
||||
return lastBefore;
|
||||
}
|
||||
lastBefore = placement;
|
||||
}
|
||||
return lastBefore;
|
||||
}
|
||||
|
||||
Rect contentRectForBounds(Size bounds) {
|
||||
if (canvasSize.width <= 0 || canvasSize.height <= 0) {
|
||||
return Offset.zero & bounds;
|
||||
}
|
||||
final scale = math.min(
|
||||
bounds.width / canvasSize.width,
|
||||
bounds.height / canvasSize.height,
|
||||
);
|
||||
final width = canvasSize.width * scale;
|
||||
final height = canvasSize.height * scale;
|
||||
return Rect.fromLTWH(
|
||||
(bounds.width - width) / 2,
|
||||
(bounds.height - height) / 2,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
Rect scaledRectForPlacement(
|
||||
RoomCpRelationFormedVapPlacement placement,
|
||||
Size bounds,
|
||||
) {
|
||||
final contentRect = contentRectForBounds(bounds);
|
||||
final scale = contentRect.width / canvasSize.width;
|
||||
return Rect.fromLTWH(
|
||||
contentRect.left + placement.frame.left * scale,
|
||||
contentRect.top + placement.frame.top * scale,
|
||||
placement.frame.width * scale,
|
||||
placement.frame.height * scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class RoomCpRelationFormedVapPlacement {
|
||||
const RoomCpRelationFormedVapPlacement({
|
||||
required this.frameIndex,
|
||||
required this.tag,
|
||||
required this.frame,
|
||||
});
|
||||
|
||||
final int frameIndex;
|
||||
final String tag;
|
||||
final Rect frame;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
RoomCpRelationFormedVapLayout? parseRoomCpRelationFormedVapLayout(
|
||||
Uint8List bytes,
|
||||
) {
|
||||
final jsonData = _extractVapcJsonData(bytes);
|
||||
if (jsonData == null) {
|
||||
return null;
|
||||
}
|
||||
final decoded = json.decode(utf8.decode(jsonData));
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
return null;
|
||||
}
|
||||
final info = decoded["info"];
|
||||
if (info is! Map<String, dynamic>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final canvasWidth = _doubleValue(info["w"]) ?? 0;
|
||||
final canvasHeight = _doubleValue(info["h"]) ?? 0;
|
||||
if (canvasWidth <= 0 || canvasHeight <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final srcIdToTag = <String, String>{};
|
||||
final sources = decoded["src"];
|
||||
if (sources is List) {
|
||||
for (final source in sources) {
|
||||
if (source is! Map) {
|
||||
continue;
|
||||
}
|
||||
final srcId = source["srcId"]?.toString().trim() ?? "";
|
||||
final tag = source["srcTag"]?.toString().trim() ?? "";
|
||||
if (srcId.isNotEmpty && tag.isNotEmpty) {
|
||||
srcIdToTag[srcId] = tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final placementsByTag = <String, List<RoomCpRelationFormedVapPlacement>>{};
|
||||
final frames = decoded["frame"];
|
||||
if (frames is List) {
|
||||
for (final frameEntry in frames) {
|
||||
if (frameEntry is! Map) {
|
||||
continue;
|
||||
}
|
||||
final frameIndex = _intValue(frameEntry["i"]);
|
||||
if (frameIndex == null) {
|
||||
continue;
|
||||
}
|
||||
final objects = frameEntry["obj"];
|
||||
if (objects is! List) {
|
||||
continue;
|
||||
}
|
||||
for (final object in objects) {
|
||||
if (object is! Map) {
|
||||
continue;
|
||||
}
|
||||
final tag = srcIdToTag[object["srcId"]?.toString().trim() ?? ""];
|
||||
final frame = _rectValue(object["frame"]);
|
||||
if (tag == null || frame == null || frame.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
placementsByTag
|
||||
.putIfAbsent(tag, () => <RoomCpRelationFormedVapPlacement>[])
|
||||
.add(
|
||||
RoomCpRelationFormedVapPlacement(
|
||||
frameIndex: frameIndex,
|
||||
tag: tag,
|
||||
frame: frame,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
placementsByTag.updateAll((_, placements) {
|
||||
placements.sort((a, b) => a.frameIndex.compareTo(b.frameIndex));
|
||||
return placements;
|
||||
});
|
||||
|
||||
final orderedTags =
|
||||
placementsByTag.keys.toList()..sort(
|
||||
(a, b) => _averageCenterX(
|
||||
placementsByTag[a]!,
|
||||
).compareTo(_averageCenterX(placementsByTag[b]!)),
|
||||
);
|
||||
|
||||
return RoomCpRelationFormedVapLayout(
|
||||
canvasSize: Size(canvasWidth, canvasHeight),
|
||||
fps: _doubleValue(info["fps"]) ?? 25,
|
||||
totalFrames: _intValue(info["f"]) ?? 125,
|
||||
placementsByTag: placementsByTag,
|
||||
leftTag: orderedTags.isEmpty ? null : orderedTags.first,
|
||||
rightTag: orderedTags.length < 2 ? null : orderedTags.last,
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List? _extractVapcJsonData(Uint8List bytes) {
|
||||
final marker = utf8.encode("vapc");
|
||||
var searchStart = 0;
|
||||
while (searchStart < bytes.length) {
|
||||
final markerIndex = _indexOfBytes(bytes, marker, searchStart);
|
||||
if (markerIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
final scanEnd = math.min(bytes.length, markerIndex + 128);
|
||||
int? jsonStart;
|
||||
for (var index = markerIndex + marker.length; index < scanEnd; index++) {
|
||||
if (bytes[index] == 0x7B) {
|
||||
jsonStart = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (jsonStart != null) {
|
||||
final jsonEnd = _findJsonEnd(bytes, jsonStart);
|
||||
if (jsonEnd != null) {
|
||||
return Uint8List.fromList(bytes.sublist(jsonStart, jsonEnd + 1));
|
||||
}
|
||||
}
|
||||
searchStart = markerIndex + marker.length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int _indexOfBytes(Uint8List bytes, List<int> marker, int start) {
|
||||
if (marker.isEmpty || bytes.length < marker.length) {
|
||||
return -1;
|
||||
}
|
||||
for (var index = start; index <= bytes.length - marker.length; index++) {
|
||||
var matched = true;
|
||||
for (var offset = 0; offset < marker.length; offset++) {
|
||||
if (bytes[index + offset] != marker[offset]) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int? _findJsonEnd(Uint8List bytes, int start) {
|
||||
var depth = 0;
|
||||
var inString = false;
|
||||
var escaped = false;
|
||||
for (var index = start; index < bytes.length; index++) {
|
||||
final byte = bytes[index];
|
||||
if (inString) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (byte == 0x5C) {
|
||||
escaped = true;
|
||||
} else if (byte == 0x22) {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (byte == 0x22) {
|
||||
inString = true;
|
||||
} else if (byte == 0x7B) {
|
||||
depth++;
|
||||
} else if (byte == 0x7D) {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Rect? _rectValue(dynamic value) {
|
||||
if (value is! List || value.length < 4) {
|
||||
return null;
|
||||
}
|
||||
final x = _doubleValue(value[0]);
|
||||
final y = _doubleValue(value[1]);
|
||||
final width = _doubleValue(value[2]);
|
||||
final height = _doubleValue(value[3]);
|
||||
if (x == null || y == null || width == null || height == null) {
|
||||
return null;
|
||||
}
|
||||
return Rect.fromLTWH(x, y, width, height);
|
||||
}
|
||||
|
||||
double _averageCenterX(List<RoomCpRelationFormedVapPlacement> placements) {
|
||||
if (placements.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
final sum = placements.fold<double>(
|
||||
0,
|
||||
(value, placement) => value + placement.frame.center.dx,
|
||||
);
|
||||
return sum / placements.length;
|
||||
}
|
||||
|
||||
int? _intValue(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.round();
|
||||
}
|
||||
return int.tryParse(value?.toString() ?? "");
|
||||
}
|
||||
|
||||
double? _doubleValue(dynamic value) {
|
||||
if (value is num) {
|
||||
return value.toDouble();
|
||||
}
|
||||
return double.tryParse(value?.toString() ?? "");
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter_debouncer/flutter_debouncer.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
@ -117,7 +118,7 @@ class _FloatingGiftScreenWidgetState extends State<FloatingGiftScreenWidget>
|
||||
return _buildGiftAnimation();
|
||||
}
|
||||
|
||||
bool get _isCpGift => (widget.message.giftTab ?? "").toUpperCase() == "CP";
|
||||
bool get _isCpGift => scIsCpGiftTab(widget.message.giftTab);
|
||||
|
||||
///礼物总价钱达到10000
|
||||
Widget _buildGiftAnimation() {
|
||||
|
||||
@ -75,13 +75,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
roomId =
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomProfile?.id ??
|
||||
"";
|
||||
userProvider?.roomUserCard(roomId, widget.userId ?? "");
|
||||
_loadRoomUserCard();
|
||||
SCAccountRepository().userIdentity(userId: widget.userId).then((v) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -91,10 +85,24 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
});
|
||||
}
|
||||
|
||||
void _loadRoomUserCard() {
|
||||
userProvider ??= Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
roomId =
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomProfile?.id ??
|
||||
"";
|
||||
userProvider?.roomUserCard(roomId, widget.userId ?? "");
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
userProvider?.userCardInfo = null;
|
||||
userProvider?.clearRoomUserCard();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -114,6 +122,10 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
: -1;
|
||||
final canLeaveMic = currentMicIndex > -1;
|
||||
|
||||
if (ref.userCardInfo == null && ref.userCardLoadError != null) {
|
||||
return _buildErrorSheet(defaultSheetHeight);
|
||||
}
|
||||
|
||||
if (ref.userCardInfo == null) {
|
||||
return _buildLoadingSheet(defaultSheetHeight);
|
||||
}
|
||||
@ -138,6 +150,52 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorSheet(double sheetHeight) {
|
||||
final localizations = SCAppLocalizations.of(context);
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: sheetHeight,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: _buildDefaultSheetBackground()),
|
||||
Center(
|
||||
child: SCDebounceWidget(
|
||||
onTap: _loadRoomUserCard,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 18.w,
|
||||
vertical: 14.w,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.refresh, color: Colors.white70, size: 28.w),
|
||||
SizedBox(height: 8.w),
|
||||
text(
|
||||
localizations?.loadingFailedClickToRetry ??
|
||||
'Loading failed, click to retry',
|
||||
textAlign: TextAlign.center,
|
||||
textColor: Colors.white70,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingSheet(double sheetHeight) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
|
||||
29
test/network_response_data_test.dart
Normal file
29
test/network_response_data_test.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
||||
|
||||
void main() {
|
||||
group('ResponseData', () {
|
||||
test('treats numeric code 200 responses as successful', () {
|
||||
final response = ResponseData<Map<String, dynamic>>.fromJson({
|
||||
'code': 200,
|
||||
'message': 'OK',
|
||||
'data': {'id': 'user-1'},
|
||||
}, fromJsonT: (json) => Map<String, dynamic>.from(json as Map));
|
||||
|
||||
expect(response.success, isTrue);
|
||||
expect(response.body?['id'], 'user-1');
|
||||
});
|
||||
|
||||
test('treats numeric code 500 responses as failed business responses', () {
|
||||
final response = ResponseData.fromJson({
|
||||
'code': 500,
|
||||
'message': 'profile parse failed',
|
||||
'data': null,
|
||||
});
|
||||
|
||||
expect(response.success, isFalse);
|
||||
expect(response.errorCode, 500);
|
||||
expect(response.errorMsg, 'profile parse failed');
|
||||
});
|
||||
});
|
||||
}
|
||||
55
test/room_cp_relation_formed_effect_layout_test.dart
Normal file
55
test/room_cp_relation_formed_effect_layout_test.dart
Normal file
@ -0,0 +1,55 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_relation_formed_effect.dart';
|
||||
|
||||
void main() {
|
||||
test('reads avatar slots from the CP formed MP4 vap metadata', () {
|
||||
final bytes =
|
||||
File(
|
||||
'sc_images/room/cp_invite/sc_cp_relation_formed_effect.mp4',
|
||||
).readAsBytesSync();
|
||||
final layout = parseRoomCpRelationFormedVapLayout(bytes);
|
||||
|
||||
expect(layout, isNotNull);
|
||||
expect(layout!.canvasSize.width, 720);
|
||||
expect(layout.canvasSize.height, 1280);
|
||||
expect(layout.fps, 25);
|
||||
expect(layout.totalFrames, 125);
|
||||
expect(layout.firstDynamicFrame, 44);
|
||||
expect(layout.lastDynamicFrame, 95);
|
||||
|
||||
final left = layout.leftPlacementForFrame(52);
|
||||
final right = layout.rightPlacementForFrame(52);
|
||||
expect(left?.frame.left, 230);
|
||||
expect(left?.frame.top, 842);
|
||||
expect(left?.frame.width, 121);
|
||||
expect(left?.frame.height, 121);
|
||||
expect(right?.frame.left, 365);
|
||||
expect(right?.frame.top, 843);
|
||||
expect(right?.frame.width, 121);
|
||||
expect(right?.frame.height, 120);
|
||||
});
|
||||
|
||||
test('scales avatar slots using the visible 720x1280 video canvas', () {
|
||||
final bytes =
|
||||
File(
|
||||
'sc_images/room/cp_invite/sc_cp_relation_formed_effect.mp4',
|
||||
).readAsBytesSync();
|
||||
final layout = parseRoomCpRelationFormedVapLayout(bytes)!;
|
||||
final left = layout.leftPlacementForFrame(52)!;
|
||||
|
||||
final contentRect = layout.contentRectForBounds(const Size(402, 874));
|
||||
final scaled = layout.scaledRectForPlacement(left, const Size(402, 874));
|
||||
|
||||
expect(contentRect.left, moreOrLessEquals(0, epsilon: 0.01));
|
||||
expect(contentRect.top, moreOrLessEquals(79.67, epsilon: 0.01));
|
||||
expect(contentRect.width, moreOrLessEquals(402, epsilon: 0.01));
|
||||
expect(contentRect.height, moreOrLessEquals(714.67, epsilon: 0.01));
|
||||
expect(scaled.left, moreOrLessEquals(128.42, epsilon: 0.01));
|
||||
expect(scaled.top, moreOrLessEquals(549.78, epsilon: 0.01));
|
||||
expect(scaled.width, moreOrLessEquals(67.56, epsilon: 0.01));
|
||||
expect(scaled.height, moreOrLessEquals(67.56, epsilon: 0.01));
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user