Compare commits
3 Commits
f2e9ac3c60
...
77d1fd13b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77d1fd13b1 | ||
|
|
bd6d35ee1b | ||
|
|
3103d51d4c |
@ -288,6 +288,7 @@
|
||||
"rechargeAgency": "وكالة الشحن",
|
||||
"logout": "تسجيل الخروج",
|
||||
"adminCenter": "مركز الإدارة",
|
||||
"managerCenter": "مركز المدير",
|
||||
"explore": "استكشاف",
|
||||
"micManagement": "إدارة الميكروفون",
|
||||
"followList": "قائمة المتابعة",
|
||||
@ -719,5 +720,11 @@
|
||||
"enterRoomFailedRetry": "فشل دخول الغرفة. يرجى المحاولة مرة أخرى.",
|
||||
"voiceConnectionFailedRetry": "فشل الاتصال الصوتي. يرجى المحاولة مرة أخرى.",
|
||||
"doYouWantToKeepTheDraft": "هل تريد الاحتفاظ بالمسودة؟",
|
||||
"duration2": "المدة:{1}"
|
||||
}
|
||||
"duration2": "المدة:{1}",
|
||||
"roomRocketNoteTitle": "ملاحظة",
|
||||
"roomRocketNoteSectionTitle": "في كل مرة يتم فيها شراء مستوى VIP",
|
||||
"roomRocketNoteSectionBody": "1. في كل مرة يتم فيها شراء مستوى VIP، تكون صلاحيته 30 يومًا، ويمكن تجديده يدويًا بعد انتهاء الصلاحية.",
|
||||
"roomRocketRewardEmptyText": "لم تحصل على مكافآت. حظًا أوفر في المرة القادمة.",
|
||||
"roomRocketRecordEmptyText": "لا توجد سجلات مكافآت",
|
||||
"launchRocket": "إطلاق الصاروخ"
|
||||
}
|
||||
|
||||
@ -442,6 +442,7 @@
|
||||
"bdCenter": "BD সেন্টার",
|
||||
"rechargeAgency": "রিচার্জ এজেন্সি",
|
||||
"adminCenter": "অ্যাডমিন সেন্টার",
|
||||
"managerCenter": "ম্যানেজার সেন্টার",
|
||||
"goldList": "সোনা তালিকা",
|
||||
"followList": "ফলো তালিকা",
|
||||
"fansList": "ফ্যান তালিকা",
|
||||
@ -719,5 +720,11 @@
|
||||
"roomMusicListMode": "ক্রম অনুসারে চালান",
|
||||
"roomMusicShuffleMode": "শাফল চালান",
|
||||
"roomMusicSingleMode": "একক গান পুনরাবৃত্তি",
|
||||
"duration2": "সময়:{1}"
|
||||
"duration2": "সময়:{1}",
|
||||
"roomRocketNoteTitle": "নোট",
|
||||
"roomRocketNoteSectionTitle": "প্রতিবার VIP লেভেল কেনার সময়",
|
||||
"roomRocketNoteSectionBody": "1. প্রতিবার VIP লেভেল কেনার পর এটি ৩০ দিনের জন্য বৈধ থাকে এবং মেয়াদ শেষ হলে ম্যানুয়ালি নবায়ন করা যায়।",
|
||||
"roomRocketRewardEmptyText": "কোনো পুরস্কার পাওয়া যায়নি। পরের বার শুভকামনা।",
|
||||
"roomRocketRecordEmptyText": "কোনো পুরস্কারের রেকর্ড নেই",
|
||||
"launchRocket": "রকেট চালু করুন"
|
||||
}
|
||||
|
||||
@ -412,6 +412,7 @@
|
||||
"bdCenter": "BD Center",
|
||||
"rechargeAgency": "Recharge Agency",
|
||||
"adminCenter": "Admin Center",
|
||||
"managerCenter": "Manager Center",
|
||||
"goldList": "Gold List",
|
||||
"followList": "Follow List",
|
||||
"fansList": "Fans List",
|
||||
@ -719,5 +720,11 @@
|
||||
"roomMusicListMode": "Play in order",
|
||||
"roomMusicShuffleMode": "Shuffle play",
|
||||
"roomMusicSingleMode": "Single loop",
|
||||
"duration2": "Duration:{1}"
|
||||
"duration2": "Duration:{1}",
|
||||
"roomRocketNoteTitle": "Note",
|
||||
"roomRocketNoteSectionTitle": "Each time a VIP",
|
||||
"roomRocketNoteSectionBody": "1. Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration.",
|
||||
"roomRocketRewardEmptyText": "No rewards earned. Better luck next time.",
|
||||
"roomRocketRecordEmptyText": "No reward records",
|
||||
"launchRocket": "Launch Rocket"
|
||||
}
|
||||
|
||||
@ -411,6 +411,7 @@
|
||||
"bdCenter": "BD Merkezi",
|
||||
"rechargeAgency": "Yükleme Temsilciliği",
|
||||
"adminCenter": "Yönetici Merkezi",
|
||||
"managerCenter": "Manager Center",
|
||||
"goldList": "Altın Listesi",
|
||||
"followList": "Takip Listesi",
|
||||
"fansList": "Hayran Listesi",
|
||||
@ -719,5 +720,11 @@
|
||||
"roomMusicListMode": "Sırayla çal",
|
||||
"roomMusicShuffleMode": "Karışık çal",
|
||||
"roomMusicSingleMode": "Tek şarkı döngüsü",
|
||||
"duration2": "Süre:{1}"
|
||||
"duration2": "Süre:{1}",
|
||||
"roomRocketNoteTitle": "Not",
|
||||
"roomRocketNoteSectionTitle": "Her VIP seviyesi satın alındığında",
|
||||
"roomRocketNoteSectionBody": "1. Her VIP seviyesi satın alındığında 30 gün geçerlidir ve süresi dolduktan sonra manuel olarak yenilenebilir.",
|
||||
"roomRocketRewardEmptyText": "Ödül kazanmadın. Bir dahaki sefere bol şans.",
|
||||
"roomRocketRecordEmptyText": "Ödül kaydı yok",
|
||||
"launchRocket": "Roketi Fırlat"
|
||||
}
|
||||
|
||||
@ -189,6 +189,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- iris_method_channel (0.0.1):
|
||||
- Flutter
|
||||
- libpag (4.5.52)
|
||||
- libwebp (1.5.0):
|
||||
- libwebp/demux (= 1.5.0)
|
||||
- libwebp/mux (= 1.5.0)
|
||||
@ -214,6 +215,9 @@ PODS:
|
||||
- SwiftyBeaver
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- pag (0.0.1):
|
||||
- Flutter
|
||||
- libpag
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
@ -275,6 +279,7 @@ DEPENDENCIES:
|
||||
- iris_method_channel (from `.symlinks/plugins/iris_method_channel/ios`)
|
||||
- on_audio_query_ios (from `.symlinks/plugins/on_audio_query_ios/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- pag (from `.symlinks/plugins/pag/ios`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
||||
@ -311,6 +316,7 @@ SPEC REPOS:
|
||||
- GTMAppAuth
|
||||
- GTMSessionFetcher
|
||||
- HydraAsync
|
||||
- libpag
|
||||
- libwebp
|
||||
- Mantle
|
||||
- nanopb
|
||||
@ -358,6 +364,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/on_audio_query_ios/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
pag:
|
||||
:path: ".symlinks/plugins/pag/ios"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
shared_preferences_foundation:
|
||||
@ -419,11 +427,13 @@ SPEC CHECKSUMS:
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
||||
iris_method_channel: 7d661cf3259b3009ae423508470dbeb9374446ee
|
||||
libpag: 98742fad4b3ac2a2ee31e383d2483495fb9a5fb4
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
on_audio_query_ios: 28a780e2d0d85d92d500ba6e12c6c8167022b2fa
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
pag: f01aa9017ab0e04a83ba4a20d6070a50f0ac9da8
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
|
||||
@ -502,7 +502,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 140;
|
||||
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -511,7 +511,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.1;
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -693,7 +693,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 140;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -702,7 +702,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.1;
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -722,7 +722,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 140;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -731,7 +731,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.1;
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@ -46,6 +46,9 @@ abstract class AppConfig {
|
||||
/// 管理员URL
|
||||
String get adminUrl;
|
||||
|
||||
/// Yumi管理中心URL
|
||||
String get managerCenterUrl;
|
||||
|
||||
/// 代理中心URL
|
||||
String get agencyCenterUrl;
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
|
||||
@override
|
||||
String get apiHost => const String.fromEnvironment(
|
||||
'API_HOST',
|
||||
defaultValue: 'https://jvapi.haiyihy.com/',
|
||||
defaultValue: 'http://192.168.110.64:1100/',
|
||||
); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 本地“http://192.168.110.64:1100/”,线上“https://jvapi.haiyihy.com/”
|
||||
|
||||
@override
|
||||
@ -55,6 +55,10 @@ class SCVariant1Config implements AppConfig {
|
||||
@override
|
||||
String get adminUrl => 'https://h5.haiyihy.com/admin-center/index.html'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get managerCenterUrl =>
|
||||
'https://h5.haiyihy.com/hyapp/manager-center/index.html'; // 正式 H5 页面
|
||||
|
||||
@override
|
||||
String get agencyCenterUrl =>
|
||||
'https://h5.haiyihy.com/agency-center/index.html'; // 正式 H5 页面
|
||||
@ -136,6 +140,7 @@ class SCVariant1Config implements AppConfig {
|
||||
'imgHost': imgHost,
|
||||
'privacyAgreementUrl': privacyAgreementUrl,
|
||||
'userAgreementUrl': userAgreementUrl,
|
||||
'managerCenterUrl': managerCenterUrl,
|
||||
'primaryColor': primaryColor,
|
||||
'tencentImAppid': tencentImAppid,
|
||||
'agoraRtcAppid': agoraRtcAppid,
|
||||
|
||||
@ -20,6 +20,7 @@ class SCGlobalConfig {
|
||||
static String get bdLeaderUrl => AppConfig.current.bdLeaderUrl;
|
||||
static String get coinSellerUrl => AppConfig.current.coinSellerUrl;
|
||||
static String get adminUrl => AppConfig.current.adminUrl;
|
||||
static String get managerCenterUrl => AppConfig.current.managerCenterUrl;
|
||||
static String get agencyCenterUrl => AppConfig.current.agencyCenterUrl;
|
||||
static String get gamesKingUrl => AppConfig.current.gamesKingUrl;
|
||||
|
||||
|
||||
@ -79,6 +79,18 @@ class SCRoomMsgType {
|
||||
///用户火箭中奖
|
||||
static const String rocketRewardUser = "ROCKET_REWARD_USER";
|
||||
|
||||
///语音房火箭状态更新
|
||||
static const String voiceRoomRocketStatusUpdate =
|
||||
"VOICE_ROOM_ROCKET_STATUS_UPDATE";
|
||||
|
||||
///语音房火箭发射广播
|
||||
static const String voiceRoomRocketLaunchBroadcast =
|
||||
"VOICE_ROOM_ROCKET_LAUNCH_BROADCAST";
|
||||
|
||||
///语音房火箭中奖弹窗提醒
|
||||
static const String voiceRoomRocketRewardPopup =
|
||||
"VOICE_ROOM_ROCKET_REWARD_POPUP";
|
||||
|
||||
///管理变动
|
||||
static const String managerList = "MANAGER_LIST";
|
||||
|
||||
|
||||
@ -799,6 +799,8 @@ class SCAppLocalizations {
|
||||
|
||||
String get launchedARocket => translate('launchedARocket');
|
||||
|
||||
String get launchRocket => translate('launchRocket');
|
||||
|
||||
String get useCoupontips => translate('useCoupontips');
|
||||
|
||||
String get joinMemberTips2 => translate('joinMemberTips2');
|
||||
@ -1025,6 +1027,8 @@ class SCAppLocalizations {
|
||||
|
||||
String get adminCenter => translate('adminCenter');
|
||||
|
||||
String get managerCenter => translate('managerCenter');
|
||||
|
||||
String get confirm => translate('confirm');
|
||||
|
||||
String get cancel => translate('cancel');
|
||||
|
||||
72
lib/debug/room_cp_progress_dialog_preview.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_progress_dialog.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const RoomCpProgressDialogPreviewApp());
|
||||
}
|
||||
|
||||
class RoomCpProgressDialogPreviewApp extends StatelessWidget {
|
||||
const RoomCpProgressDialogPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
splitScreenMode: true,
|
||||
builder:
|
||||
(_, child) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: FlutterSmartDialog.init(),
|
||||
home: const _RoomCpProgressDialogPreviewPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomCpProgressDialogPreviewPage extends StatelessWidget {
|
||||
const _RoomCpProgressDialogPreviewPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xff1B1224),
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xff281B35), Color(0xff120B19)],
|
||||
),
|
||||
),
|
||||
),
|
||||
const RoomCpProgressDialog(
|
||||
daysValue: '999',
|
||||
daysLabel: 'Days',
|
||||
levelText: 'Lv.1 Simple Love',
|
||||
currentValue: 1,
|
||||
targetValue: 1,
|
||||
expAwayText: '1,000',
|
||||
nextLevelText: 'Lv. 2',
|
||||
cancelText: 'Cancel',
|
||||
leftUser: RoomCpProgressDialogUser(
|
||||
nickname: 'NameName',
|
||||
userId: '123456',
|
||||
levelText: 'Lv.1',
|
||||
),
|
||||
rightUser: RoomCpProgressDialogUser(
|
||||
nickname: 'NameName',
|
||||
userId: '123456',
|
||||
levelText: 'Lv.1',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
160
lib/debug/room_rocket_dialog_preview.dart
Normal file
@ -0,0 +1,160 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const RoomRocketDialogPreviewApp());
|
||||
}
|
||||
|
||||
class RoomRocketDialogPreviewApp extends StatelessWidget {
|
||||
const RoomRocketDialogPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
splitScreenMode: true,
|
||||
builder:
|
||||
(_, child) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: FlutterSmartDialog.init(),
|
||||
home: const _RoomRocketDialogPreviewPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketDialogPreviewPage extends StatefulWidget {
|
||||
const _RoomRocketDialogPreviewPage();
|
||||
|
||||
@override
|
||||
State<_RoomRocketDialogPreviewPage> createState() =>
|
||||
_RoomRocketDialogPreviewPageState();
|
||||
}
|
||||
|
||||
class _RoomRocketDialogPreviewPageState
|
||||
extends State<_RoomRocketDialogPreviewPage> {
|
||||
RoomRocketRewardItem? _selectedReward;
|
||||
_PreviewDialog _dialog = _PreviewDialog.rocket;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const _RoomBackground(),
|
||||
if (_dialog == _PreviewDialog.rocket)
|
||||
RoomRocketDialog(
|
||||
onRewardSelected:
|
||||
(reward) => setState(() {
|
||||
_selectedReward = reward;
|
||||
_dialog = _PreviewDialog.reward;
|
||||
}),
|
||||
onOpenRecord:
|
||||
() => setState(() => _dialog = _PreviewDialog.record),
|
||||
onOpenNote: () => setState(() => _dialog = _PreviewDialog.note),
|
||||
)
|
||||
else if (_dialog == _PreviewDialog.reward)
|
||||
RoomRocketRewardDialog(
|
||||
selectedReward: _selectedReward,
|
||||
onClose: _backToRocket,
|
||||
)
|
||||
else if (_dialog == _PreviewDialog.record)
|
||||
RoomRocketRecordDialog(onClose: _backToRocket)
|
||||
else
|
||||
RoomRocketNoteDialog(onClose: _backToRocket),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _backToRocket() {
|
||||
setState(() {
|
||||
_selectedReward = null;
|
||||
_dialog = _PreviewDialog.rocket;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum _PreviewDialog { rocket, reward, record, note }
|
||||
|
||||
class _RoomBackground extends StatelessWidget {
|
||||
const _RoomBackground();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF2A120E), Color(0xFF071917), Color(0xFF22140B)],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 20.w,
|
||||
top: 48.w,
|
||||
child: _PreviewLine(width: 104.w, opacity: 0.42),
|
||||
),
|
||||
for (var index = 0; index < 8; index++)
|
||||
Positioned(
|
||||
left: (40 + (index % 4) * 72).w,
|
||||
top: (146 + (index ~/ 4) * 84).w,
|
||||
child: const _PreviewSeat(),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 118.w,
|
||||
child: _PreviewLine(width: 330.w, opacity: 0.28),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewSeat extends StatelessWidget {
|
||||
const _PreviewSeat();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFFCE7C).withValues(alpha: 0.32),
|
||||
width: 2.w,
|
||||
),
|
||||
),
|
||||
child: SizedBox(width: 44.w, height: 44.w),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewLine extends StatelessWidget {
|
||||
const _PreviewLine({required this.width, required this.opacity});
|
||||
|
||||
final double width;
|
||||
final double opacity;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: width,
|
||||
height: 1.w,
|
||||
color: Colors.white.withValues(alpha: opacity),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
71
lib/debug/room_rocket_empty_dialog_preview.dart
Normal file
@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const RoomRocketEmptyDialogPreviewApp());
|
||||
}
|
||||
|
||||
class RoomRocketEmptyDialogPreviewApp extends StatelessWidget {
|
||||
const RoomRocketEmptyDialogPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
splitScreenMode: true,
|
||||
builder:
|
||||
(_, child) => const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: _RoomRocketEmptyDialogPreviewPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketEmptyDialogPreviewPage extends StatelessWidget {
|
||||
const _RoomRocketEmptyDialogPreviewPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const _RoomBackground(),
|
||||
RoomRocketDialog(
|
||||
status: SCRoomRocketStatusRes(
|
||||
level: 1,
|
||||
energyPercent: 0,
|
||||
totalContributors: 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomBackground extends StatelessWidget {
|
||||
const _RoomBackground();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'sc_images/room/background_examples/bg_example_1.png',
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.24)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
66
lib/debug/room_rocket_floating_preview.dart
Normal file
@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const RoomRocketFloatingPreviewApp());
|
||||
}
|
||||
|
||||
class RoomRocketFloatingPreviewApp extends StatelessWidget {
|
||||
const RoomRocketFloatingPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
builder: (_, child) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: const _RoomRocketFloatingPreviewPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketFloatingPreviewPage extends StatelessWidget {
|
||||
const _RoomRocketFloatingPreviewPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF20100B), Color(0xFF5A2A13)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, 70.w),
|
||||
child: FloatingRoomRocketScreenWidget(
|
||||
message: SCFloatingMessage(
|
||||
type: 3,
|
||||
roomId: '123456',
|
||||
userName: 'Namenamna...',
|
||||
userAvatarUrl:
|
||||
'https://images.unsplash.com/photo-1494790108377-be9c29b29330',
|
||||
durationSeconds: 20,
|
||||
),
|
||||
onAnimationCompleted: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
65
lib/debug/room_rocket_note_dialog_preview.dart
Normal file
@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const RoomRocketNoteDialogPreviewApp());
|
||||
}
|
||||
|
||||
class RoomRocketNoteDialogPreviewApp extends StatelessWidget {
|
||||
const RoomRocketNoteDialogPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: Locale('en'),
|
||||
supportedLocales: [
|
||||
Locale('en'),
|
||||
Locale('ar'),
|
||||
Locale('bn'),
|
||||
Locale('tr'),
|
||||
],
|
||||
localizationsDelegates: [
|
||||
SCAppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
home: _RoomRocketNoteDialogPreviewPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketNoteDialogPreviewPage extends StatelessWidget {
|
||||
const _RoomRocketNoteDialogPreviewPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [_RoomBackground(), RoomRocketNoteDialog()],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomBackground extends StatelessWidget {
|
||||
const _RoomBackground();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF2A120E), Color(0xFF071917), Color(0xFF22140B)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/debug/room_rocket_rank_preview.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/modules/room/rocket/room_rocket_rank_page.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const RoomRocketRankPreviewApp());
|
||||
}
|
||||
|
||||
class RoomRocketRankPreviewApp extends StatelessWidget {
|
||||
const RoomRocketRankPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: RoomRocketRankPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
50
lib/debug/room_rocket_record_dialog_preview.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const RoomRocketRecordDialogPreviewApp());
|
||||
}
|
||||
|
||||
class RoomRocketRecordDialogPreviewApp extends StatelessWidget {
|
||||
const RoomRocketRecordDialogPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: _RoomRocketRecordDialogPreviewPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketRecordDialogPreviewPage extends StatelessWidget {
|
||||
const _RoomRocketRecordDialogPreviewPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [_RoomBackground(), RoomRocketRecordDialog()],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomBackground extends StatelessWidget {
|
||||
const _RoomBackground();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF2A120E), Color(0xFF071917), Color(0xFF22140B)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/debug/room_rocket_reward_dialog_preview.dart
Normal file
@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const RoomRocketRewardDialogPreviewApp());
|
||||
}
|
||||
|
||||
class RoomRocketRewardDialogPreviewApp extends StatelessWidget {
|
||||
const RoomRocketRewardDialogPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: _RoomRocketRewardDialogPreviewPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketRewardDialogPreviewPage extends StatelessWidget {
|
||||
const _RoomRocketRewardDialogPreviewPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_RoomBackground(),
|
||||
RoomRocketRewardDialog(
|
||||
selectedReward: RoomRocketRewardItem(
|
||||
asset: 'sc_images/room/rocket/reward_thumb.png',
|
||||
amount: '10000',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomBackground extends StatelessWidget {
|
||||
const _RoomBackground();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF2A120E), Color(0xFF071917), Color(0xFF22140B)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -46,7 +46,6 @@ import 'services/auth/user_profile_manager.dart';
|
||||
import 'ui_kit/theme/socialchat_theme.dart';
|
||||
import 'ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart';
|
||||
import 'ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart';
|
||||
import 'ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
|
||||
bool _isCrashlyticsReady = false;
|
||||
|
||||
@ -109,7 +108,6 @@ void _configureImageCache() {
|
||||
|
||||
void _releaseVisualMemoryCaches({bool clearLiveImages = false}) {
|
||||
SCGiftVapSvgaManager().clearMemoryCache();
|
||||
SCSvgaAssetWidget.clearMemoryCache();
|
||||
final imageCache = PaintingBinding.instance.imageCache;
|
||||
imageCache.clear();
|
||||
if (clearLiveImages) {
|
||||
|
||||
@ -16,7 +16,6 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_version_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
@ -376,14 +375,16 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
|
||||
}
|
||||
SCLoadingManager.show(context: context);
|
||||
try {
|
||||
final results = await Future.wait([
|
||||
SCAccountRepository().loginForAccount(account, pass),
|
||||
SCVersionUtils.checkReview(),
|
||||
]);
|
||||
var user = (results[0] as SocialChatLoginRes);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
var user = await SCAccountRepository().loginForAccount(account, pass);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
AccountStorage().setCurrentUser(user);
|
||||
final canContinue = await SCVersionUtils.checkReview(context: context);
|
||||
if (!mounted || !canContinue) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
await Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
@ -393,7 +394,6 @@ class SCLoginWithAccountPageState extends State<SCLoginWithAccountPage>
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
AccountStorage().setCurrentUser(user);
|
||||
SCLoadingManager.hide();
|
||||
try {
|
||||
await Future.wait([
|
||||
|
||||
@ -60,10 +60,12 @@ class GiftPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
static const String _backpackGiftTab = SCAppGeneralManager.backpackGiftTab;
|
||||
static const Duration _comboSendBatchWindow = Duration(milliseconds: 200);
|
||||
|
||||
static const List<String> _preferredGiftTabOrder = <String>[
|
||||
"ALL",
|
||||
_backpackGiftTab,
|
||||
"ACTIVITY",
|
||||
"LUCKY_GIFT",
|
||||
"CP",
|
||||
@ -218,7 +220,12 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
listen: false,
|
||||
).giftList(includeCustomized: false);
|
||||
Provider.of<SCAppGeneralManager>(context, listen: false).giftActivityList();
|
||||
// Provider.of<GeneralProvider>(context, listen: false).giftBackpack();
|
||||
unawaited(
|
||||
Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).giftBackpack(forceRefresh: true),
|
||||
);
|
||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||||
rtcProvider?.roomWheatMap.forEach((k, v) {
|
||||
if (v.user != null) {
|
||||
@ -262,10 +269,23 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
ref.giftByTab.entries
|
||||
.where((entry) => entry.value.isNotEmpty)
|
||||
.where((entry) => entry.key != "CUSTOMIZED")
|
||||
.where((entry) => entry.key != _backpackGiftTab)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
final orderedTypes = <String>[];
|
||||
if (availableTypes.remove("ALL") ||
|
||||
ref.isGiftListLoading ||
|
||||
ref.giftResList.isEmpty) {
|
||||
orderedTypes.add("ALL");
|
||||
}
|
||||
for (final type in _preferredGiftTabOrder) {
|
||||
if (type == "ALL") {
|
||||
continue;
|
||||
}
|
||||
if (type == _backpackGiftTab) {
|
||||
orderedTypes.add(type);
|
||||
continue;
|
||||
}
|
||||
if (availableTypes.remove(type)) {
|
||||
orderedTypes.add(type);
|
||||
}
|
||||
@ -328,7 +348,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
|
||||
void _ensureCurrentTabSelection(SCAppGeneralManager ref, String tabType) {
|
||||
final gifts = ref.giftByTab[tabType] ?? const <SocialChatGiftRes>[];
|
||||
if (_matchesCurrentGift(gifts)) {
|
||||
if (_matchesCurrentGift(tabType, gifts)) {
|
||||
return;
|
||||
}
|
||||
final gift = _resolveSelectedGift(tabType, gifts);
|
||||
@ -347,6 +367,13 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
final selectedGift = _selectedGiftByTab[tabType];
|
||||
if (selectedGift?.id != null) {
|
||||
for (final gift in gifts) {
|
||||
if (tabType == _backpackGiftTab) {
|
||||
final selectedBackpackId = selectedGift!.backpackId;
|
||||
if ((selectedBackpackId ?? "").isNotEmpty &&
|
||||
gift.backpackId == selectedBackpackId) {
|
||||
return gift;
|
||||
}
|
||||
}
|
||||
if (gift.id == selectedGift!.id) {
|
||||
return gift;
|
||||
}
|
||||
@ -359,12 +386,22 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return gifts.first;
|
||||
}
|
||||
|
||||
bool _matchesCurrentGift(List<SocialChatGiftRes> gifts) {
|
||||
bool _matchesCurrentGift(String tabType, List<SocialChatGiftRes> gifts) {
|
||||
final currentGiftId = checkedGift?.id;
|
||||
if (currentGiftId == null) {
|
||||
return gifts.isEmpty;
|
||||
}
|
||||
return gifts.any((gift) => gift.id == currentGiftId);
|
||||
for (final gift in gifts) {
|
||||
if (gift.id != currentGiftId) {
|
||||
continue;
|
||||
}
|
||||
if (tabType != _backpackGiftTab) {
|
||||
return true;
|
||||
}
|
||||
return gift.backpackId == checkedGift?.backpackId &&
|
||||
gift.backpackQuantity == checkedGift?.backpackQuantity;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String _giftTabLabel(SCAppLocalizations localizations, String tabType) {
|
||||
@ -384,6 +421,8 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return localizations.customized;
|
||||
case "NSCIONAL_FLAG":
|
||||
return localizations.country;
|
||||
case _backpackGiftTab:
|
||||
return localizations.bag;
|
||||
default:
|
||||
return tabType
|
||||
.toLowerCase()
|
||||
@ -421,6 +460,10 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
giftTab == SCGiftType.MAGIC.name;
|
||||
}
|
||||
|
||||
bool _usesBackpackGiftEndpoint(SocialChatGiftRes? gift) {
|
||||
return (gift?.giftTab ?? '').trim().toLowerCase() == _backpackGiftTab;
|
||||
}
|
||||
|
||||
bool _hasValidLuckyGiftStandardId(SocialChatGiftRes gift) {
|
||||
final standardId = (gift.standardId ?? '').trim();
|
||||
return standardId.isNotEmpty && standardId != '0';
|
||||
@ -444,7 +487,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
|
||||
if (error is DioException) {
|
||||
final responseData = error.response?.data;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
if (responseData is Map) {
|
||||
final responseMessage = sanitize(
|
||||
responseData['errorMsg']?.toString() ?? '',
|
||||
);
|
||||
@ -488,6 +531,47 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return 'Gift sending failed, please try again.';
|
||||
}
|
||||
|
||||
String _backpackQuantityInsufficientMessage() {
|
||||
return 'Gift quantity is insufficient or has been used up.';
|
||||
}
|
||||
|
||||
bool _isBackpackQuantityError(Object error) {
|
||||
final errorText = _readBackendErrorText(error).toUpperCase();
|
||||
return errorText.contains('INSUFFICIENT_BALANCE');
|
||||
}
|
||||
|
||||
String _readBackendErrorText(Object error) {
|
||||
final values = <String>[];
|
||||
void addValue(dynamic value) {
|
||||
final text = value?.toString().trim() ?? '';
|
||||
if (text.isNotEmpty) {
|
||||
values.add(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (error is DioException) {
|
||||
final responseData = error.response?.data;
|
||||
if (responseData is Map) {
|
||||
addValue(responseData['errorCodeName']);
|
||||
addValue(responseData['codeName']);
|
||||
addValue(responseData['errorCode']);
|
||||
addValue(responseData['code']);
|
||||
addValue(responseData['errorMsg']);
|
||||
addValue(responseData['message']);
|
||||
} else {
|
||||
addValue(responseData);
|
||||
}
|
||||
addValue(error.message);
|
||||
addValue(error.error);
|
||||
} else if (error is NotSuccessException) {
|
||||
addValue(error.message);
|
||||
} else {
|
||||
addValue(error);
|
||||
}
|
||||
|
||||
return values.join(' ');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<SCAppGeneralManager>(
|
||||
@ -1108,7 +1192,51 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
final roomAccount =
|
||||
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
||||
final isLuckyGiftRequest = _usesLuckyGiftEndpoint(selectedGift);
|
||||
final isBackpackGiftRequest = _usesBackpackGiftEndpoint(selectedGift);
|
||||
final isLuckyGiftRequest =
|
||||
!isBackpackGiftRequest && _usesLuckyGiftEndpoint(selectedGift);
|
||||
|
||||
if (isBackpackGiftRequest) {
|
||||
final backpackRecordId = (selectedGift.backpackId ?? "").trim();
|
||||
final giftId = (selectedGift.id ?? "").trim();
|
||||
if (backpackRecordId.isEmpty || giftId.isEmpty) {
|
||||
const configError =
|
||||
'Gift configuration unavailable, please try another gift.';
|
||||
_giftFxLog(
|
||||
'giveBackpackGift skipped giftId=${selectedGift.id} '
|
||||
'giftName=${selectedGift.giftName} '
|
||||
'backpackId=${selectedGift.backpackId} '
|
||||
'reason=invalid_backpack_config',
|
||||
);
|
||||
SCTts.show(configError);
|
||||
return null;
|
||||
}
|
||||
final availableQuantity =
|
||||
selectedGift.backpackQuantity?.toInt() ??
|
||||
int.tryParse(selectedGift.quantity ?? '') ??
|
||||
0;
|
||||
final requiredQuantity = selectedNumber * acceptUserIds.length;
|
||||
if (requiredQuantity > availableQuantity) {
|
||||
_giftFxLog(
|
||||
'giveBackpackGift skipped giftId=${selectedGift.id} '
|
||||
'giftName=${selectedGift.giftName} '
|
||||
'backpackId=${selectedGift.backpackId} '
|
||||
'selectedNumber=$selectedNumber '
|
||||
'acceptCount=${acceptUserIds.length} '
|
||||
'requiredQuantity=$requiredQuantity '
|
||||
'availableQuantity=$availableQuantity '
|
||||
'reason=insufficient_backpack_quantity',
|
||||
);
|
||||
SCTts.show(_backpackQuantityInsufficientMessage());
|
||||
unawaited(
|
||||
Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).giftBackpack(forceRefresh: true),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (isLuckyGiftRequest && !_hasValidLuckyGiftStandardId(selectedGift)) {
|
||||
const configError =
|
||||
@ -1134,6 +1262,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
roomId: roomId,
|
||||
roomAccount: roomAccount,
|
||||
isLuckyGiftRequest: isLuckyGiftRequest,
|
||||
isBackpackGiftRequest: isBackpackGiftRequest,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1144,19 +1273,23 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return;
|
||||
}
|
||||
|
||||
RoomGiftComboSendController().show(
|
||||
request: RoomGiftComboSendRequest(
|
||||
acceptUserIds: List<String>.from(request.acceptUserIds),
|
||||
acceptUsers: List<MicRes>.from(request.acceptUsers),
|
||||
gift: request.gift,
|
||||
quantity: request.quantity,
|
||||
clickCount: request.clickCount,
|
||||
roomId: request.roomId,
|
||||
roomAccount: request.roomAccount,
|
||||
isLuckyGiftRequest: request.isLuckyGiftRequest,
|
||||
),
|
||||
executor: _performFloatingComboSend,
|
||||
);
|
||||
if (request.isBackpackGiftRequest) {
|
||||
RoomGiftComboSendController().hide();
|
||||
} else {
|
||||
RoomGiftComboSendController().show(
|
||||
request: RoomGiftComboSendRequest(
|
||||
acceptUserIds: List<String>.from(request.acceptUserIds),
|
||||
acceptUsers: List<MicRes>.from(request.acceptUsers),
|
||||
gift: request.gift,
|
||||
quantity: request.quantity,
|
||||
clickCount: request.clickCount,
|
||||
roomId: request.roomId,
|
||||
roomAccount: request.roomAccount,
|
||||
isLuckyGiftRequest: request.isLuckyGiftRequest,
|
||||
),
|
||||
executor: _performFloatingComboSend,
|
||||
);
|
||||
}
|
||||
|
||||
if (_supportsComboRequestBatching(request.gift)) {
|
||||
_enqueueComboGiftSendRequest(request);
|
||||
@ -1168,7 +1301,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
bool _supportsComboRequestBatching(SocialChatGiftRes gift) {
|
||||
return _supportsComboFeedback(gift);
|
||||
return !_usesBackpackGiftEndpoint(gift) && _supportsComboFeedback(gift);
|
||||
}
|
||||
|
||||
void _enqueueComboGiftSendRequest(_GiftSendRequest request) {
|
||||
@ -1315,6 +1448,40 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
'quantity=${request.quantity} '
|
||||
'clickCount=${request.clickCount}',
|
||||
);
|
||||
if (request.isBackpackGiftRequest) {
|
||||
final sentUsers = await _giveBackpackGiftToRecipients(
|
||||
repository,
|
||||
request,
|
||||
);
|
||||
_giftFxLog(
|
||||
'$requestName success trigger=$trigger '
|
||||
'giftId=${request.gift.id} '
|
||||
'giftName=${request.gift.giftName} '
|
||||
'giftSourceUrl=${request.gift.giftSourceUrl} '
|
||||
'special=${request.gift.special} '
|
||||
'giftTab=${request.gift.giftTab} '
|
||||
'backpackId=${request.gift.backpackId} '
|
||||
'backpackQuantity=${request.gift.backpackQuantity} '
|
||||
'number=${request.quantity} '
|
||||
'clickCount=${request.clickCount} '
|
||||
'acceptUserIds=${request.acceptUserIds.join(",")} '
|
||||
'roomId=${request.roomId} '
|
||||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||||
);
|
||||
if (sentUsers.isNotEmpty) {
|
||||
final giftBatchId = _createGiftBatchId(request);
|
||||
sendGiftMsg(
|
||||
sentUsers,
|
||||
gift: request.gift,
|
||||
quantity: request.quantity,
|
||||
giftBatchId: giftBatchId,
|
||||
animationCount: request.clickCount,
|
||||
);
|
||||
}
|
||||
await _refreshGiftBackpack();
|
||||
return;
|
||||
}
|
||||
|
||||
final result =
|
||||
request.isLuckyGiftRequest
|
||||
? await repository.giveLuckyGift(
|
||||
@ -1374,7 +1541,10 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
}
|
||||
profileManager?.updateBalance(result);
|
||||
} catch (e) {
|
||||
final errorMessage = _resolveGiftSendErrorMessage(e);
|
||||
final errorMessage =
|
||||
request.isBackpackGiftRequest && _isBackpackQuantityError(e)
|
||||
? _backpackQuantityInsufficientMessage()
|
||||
: _resolveGiftSendErrorMessage(e);
|
||||
final errorDetails = _describeGiftSendError(e);
|
||||
_giftFxLog(
|
||||
'$requestName failed trigger=$trigger '
|
||||
@ -1387,10 +1557,79 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
'details={$errorDetails} '
|
||||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||||
);
|
||||
if (request.isBackpackGiftRequest) {
|
||||
unawaited(_refreshGiftBackpack());
|
||||
}
|
||||
SCTts.show(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<MicRes>> _giveBackpackGiftToRecipients(
|
||||
SCChatRoomRepository repository,
|
||||
_GiftSendRequest request,
|
||||
) async {
|
||||
final sentUsers = <MicRes>[];
|
||||
for (var i = 0; i < request.acceptUserIds.length; i++) {
|
||||
final acceptUserId = request.acceptUserIds[i].trim();
|
||||
if (acceptUserId.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final acceptUser =
|
||||
i < request.acceptUsers.length ? request.acceptUsers[i] : null;
|
||||
try {
|
||||
await repository.giveBackpackGift(
|
||||
id: request.gift.backpackId ?? "",
|
||||
acceptUserId: acceptUserId,
|
||||
giftId: request.gift.id ?? "",
|
||||
quantity: request.quantity,
|
||||
roomId: request.roomId,
|
||||
seatIndex:
|
||||
acceptUser?.micIndex ?? _resolveSeatIndexForUserId(acceptUserId),
|
||||
);
|
||||
if (acceptUser != null) {
|
||||
sentUsers.add(acceptUser);
|
||||
}
|
||||
} catch (_) {
|
||||
if (sentUsers.isNotEmpty) {
|
||||
final giftBatchId = _createGiftBatchId(request);
|
||||
sendGiftMsg(
|
||||
sentUsers,
|
||||
gift: request.gift,
|
||||
quantity: request.quantity,
|
||||
giftBatchId: giftBatchId,
|
||||
animationCount: request.clickCount,
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
return sentUsers;
|
||||
}
|
||||
|
||||
num? _resolveSeatIndexForUserId(String userId) {
|
||||
final normalizedUserId = userId.trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
for (final mic in rtcProvider?.roomWheatMap.values ?? const <MicRes>[]) {
|
||||
if ((mic.user?.id ?? "").trim() == normalizedUserId) {
|
||||
return mic.micIndex;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _refreshGiftBackpack() async {
|
||||
final context = navigatorKey.currentState?.context;
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
await Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).giftBackpack(forceRefresh: true);
|
||||
}
|
||||
|
||||
void sendGiftMsg(
|
||||
List<MicRes> acceptUsers, {
|
||||
required SocialChatGiftRes gift,
|
||||
@ -1628,6 +1867,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
roomId: request.roomId,
|
||||
roomAccount: request.roomAccount,
|
||||
isLuckyGiftRequest: request.isLuckyGiftRequest,
|
||||
isBackpackGiftRequest: _usesBackpackGiftEndpoint(request.gift),
|
||||
),
|
||||
trigger: trigger,
|
||||
);
|
||||
@ -1752,7 +1992,11 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
final sortedAcceptUserIds = List<String>.from(request.acceptUserIds)
|
||||
..sort();
|
||||
return [
|
||||
request.isLuckyGiftRequest ? 'lucky' : 'gift',
|
||||
request.isBackpackGiftRequest
|
||||
? 'backpack'
|
||||
: request.isLuckyGiftRequest
|
||||
? 'lucky'
|
||||
: 'gift',
|
||||
request.roomId,
|
||||
request.gift.id ?? '',
|
||||
request.quantity,
|
||||
@ -1842,6 +2086,7 @@ class _GiftSendRequest {
|
||||
required this.roomId,
|
||||
required this.roomAccount,
|
||||
required this.isLuckyGiftRequest,
|
||||
required this.isBackpackGiftRequest,
|
||||
});
|
||||
|
||||
final List<String> acceptUserIds;
|
||||
@ -1852,15 +2097,30 @@ class _GiftSendRequest {
|
||||
final String roomId;
|
||||
final String roomAccount;
|
||||
final bool isLuckyGiftRequest;
|
||||
final bool isBackpackGiftRequest;
|
||||
|
||||
String get requestName => isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
|
||||
String get requestName {
|
||||
if (isBackpackGiftRequest) {
|
||||
return 'giveBackpackGift';
|
||||
}
|
||||
return isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
|
||||
}
|
||||
|
||||
String get batchKey {
|
||||
final sortedAcceptUserIds = List<String>.from(acceptUserIds)..sort();
|
||||
return '${isLuckyGiftRequest ? "lucky" : "gift"}'
|
||||
'|${gift.id ?? ""}'
|
||||
'|$roomId'
|
||||
'|${sortedAcceptUserIds.join(",")}';
|
||||
final type =
|
||||
isBackpackGiftRequest
|
||||
? "backpack"
|
||||
: isLuckyGiftRequest
|
||||
? "lucky"
|
||||
: "gift";
|
||||
return [
|
||||
type,
|
||||
gift.id ?? "",
|
||||
gift.backpackId ?? "",
|
||||
roomId,
|
||||
sortedAcceptUserIds.join(","),
|
||||
].join("|");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1875,6 +2135,7 @@ class _PendingGiftSendBatch {
|
||||
required this.roomId,
|
||||
required this.roomAccount,
|
||||
required this.isLuckyGiftRequest,
|
||||
required this.isBackpackGiftRequest,
|
||||
required this.readyAt,
|
||||
});
|
||||
|
||||
@ -1892,6 +2153,7 @@ class _PendingGiftSendBatch {
|
||||
roomId: request.roomId,
|
||||
roomAccount: request.roomAccount,
|
||||
isLuckyGiftRequest: request.isLuckyGiftRequest,
|
||||
isBackpackGiftRequest: request.isBackpackGiftRequest,
|
||||
readyAt: readyAt,
|
||||
);
|
||||
}
|
||||
@ -1905,6 +2167,7 @@ class _PendingGiftSendBatch {
|
||||
final String roomId;
|
||||
final String roomAccount;
|
||||
final bool isLuckyGiftRequest;
|
||||
final bool isBackpackGiftRequest;
|
||||
DateTime readyAt;
|
||||
|
||||
_GiftSendRequest toRequest() {
|
||||
@ -1917,6 +2180,7 @@ class _PendingGiftSendBatch {
|
||||
roomId: roomId,
|
||||
roomAccount: roomAccount,
|
||||
isLuckyGiftRequest: isLuckyGiftRequest,
|
||||
isBackpackGiftRequest: isBackpackGiftRequest,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ class _GiftTabPageState extends State<GiftTabPage>
|
||||
_schedulePageWarmup(ref, gifts, _index);
|
||||
}
|
||||
return gifts.isEmpty
|
||||
? (ref.isGiftListLoading
|
||||
? (ref.isGiftTabLoading(widget.type)
|
||||
? _buildGiftPageSkeleton()
|
||||
: mainEmpty(
|
||||
textColor: Colors.white54,
|
||||
@ -444,19 +444,33 @@ class _GiftTabPageState extends State<GiftTabPage>
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
_strategy.getGiftPageGoldCoinIcon(),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
text(
|
||||
"${gift.giftCandy}",
|
||||
fontSize: 10.sp,
|
||||
textColor: widget.isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
],
|
||||
children:
|
||||
_isBackpackTab
|
||||
? [
|
||||
text(
|
||||
"x${_formatBackpackQuantity(gift)}",
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
textColor:
|
||||
widget.isDark
|
||||
? SocialChatTheme.primaryLight
|
||||
: SocialChatTheme.primaryColor,
|
||||
),
|
||||
]
|
||||
: [
|
||||
Image.asset(
|
||||
_strategy.getGiftPageGoldCoinIcon(),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
text(
|
||||
"${gift.giftCandy}",
|
||||
fontSize: 10.sp,
|
||||
textColor:
|
||||
widget.isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -470,6 +484,19 @@ class _GiftTabPageState extends State<GiftTabPage>
|
||||
);
|
||||
}
|
||||
|
||||
bool get _isBackpackTab => widget.type == SCAppGeneralManager.backpackGiftTab;
|
||||
|
||||
String _formatBackpackQuantity(SocialChatGiftRes gift) {
|
||||
final quantity = gift.backpackQuantity;
|
||||
if (quantity != null) {
|
||||
if (quantity % 1 == 0) {
|
||||
return quantity.toInt().toString();
|
||||
}
|
||||
return quantity.toString();
|
||||
}
|
||||
return gift.quantity ?? '0';
|
||||
}
|
||||
|
||||
_indicator(SCAppGeneralManager ref) {
|
||||
var size = (ref.giftByTab[widget.type]!.length) / 8;
|
||||
var list = <Widget>[];
|
||||
|
||||
@ -203,35 +203,17 @@ class _RoomFollowPageState
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
(roomRes
|
||||
.roomProfile
|
||||
?.extValues
|
||||
?.roomSetting
|
||||
?.password
|
||||
?.isEmpty ??
|
||||
false)
|
||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
||||
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
|
||||
: Image.asset(
|
||||
"sc_images/index/sc_icon_room_suo.png",
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
),
|
||||
(roomRes
|
||||
.roomProfile
|
||||
?.extValues
|
||||
?.roomSetting
|
||||
?.password
|
||||
?.isEmpty ??
|
||||
false)
|
||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
||||
? SizedBox(width: 3.w)
|
||||
: Container(height: 10.w),
|
||||
(roomRes
|
||||
.roomProfile
|
||||
?.extValues
|
||||
?.roomSetting
|
||||
?.password
|
||||
?.isEmpty ??
|
||||
false)
|
||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
||||
? text(
|
||||
roomRes.roomProfile?.displayMemberCount ?? "0",
|
||||
fontSize: 10.sp,
|
||||
@ -363,8 +345,8 @@ class _RoomFollowPageState
|
||||
previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon &&
|
||||
previous.roomProfile?.displayMemberCount ==
|
||||
next.roomProfile?.displayMemberCount &&
|
||||
previous.roomProfile?.extValues?.roomSetting?.password ==
|
||||
next.roomProfile?.extValues?.roomSetting?.password;
|
||||
previous.roomProfile?.extValues?.existsPassword ==
|
||||
next.roomProfile?.extValues?.existsPassword;
|
||||
}
|
||||
|
||||
bool _sameRoomLists(List<FollowRoomRes> previous, List<FollowRoomRes> next) {
|
||||
|
||||
@ -204,35 +204,17 @@ class _SCRoomHistoryPageState
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
(roomRes
|
||||
.roomProfile
|
||||
?.extValues
|
||||
?.roomSetting
|
||||
?.password
|
||||
?.isEmpty ??
|
||||
false)
|
||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
||||
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
|
||||
: Image.asset(
|
||||
"sc_images/index/sc_icon_room_suo.png",
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
),
|
||||
(roomRes
|
||||
.roomProfile
|
||||
?.extValues
|
||||
?.roomSetting
|
||||
?.password
|
||||
?.isEmpty ??
|
||||
false)
|
||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
||||
? SizedBox(width: 3.w)
|
||||
: Container(height: 10.w),
|
||||
(roomRes
|
||||
.roomProfile
|
||||
?.extValues
|
||||
?.roomSetting
|
||||
?.password
|
||||
?.isEmpty ??
|
||||
false)
|
||||
roomRes.roomProfile?.extValues?.existsPassword != true
|
||||
? text(
|
||||
roomRes.roomProfile?.displayMemberCount ?? "0",
|
||||
fontSize: 10.sp,
|
||||
@ -363,8 +345,8 @@ class _SCRoomHistoryPageState
|
||||
previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon &&
|
||||
previous.roomProfile?.displayMemberCount ==
|
||||
next.roomProfile?.displayMemberCount &&
|
||||
previous.roomProfile?.extValues?.roomSetting?.password ==
|
||||
next.roomProfile?.extValues?.roomSetting?.password;
|
||||
previous.roomProfile?.extValues?.existsPassword ==
|
||||
next.roomProfile?.extValues?.existsPassword;
|
||||
}
|
||||
|
||||
bool _sameRoomLists(List<FollowRoomRes> previous, List<FollowRoomRes> next) {
|
||||
|
||||
@ -43,7 +43,7 @@ class _RoomOnlinePageState
|
||||
if (dialogContext == null) {
|
||||
return;
|
||||
}
|
||||
showBottomInCenterDialog(dialogContext, RoomUserInfoCard(userId: userId));
|
||||
showBottomInBottomDialog(dialogContext, RoomUserInfoCard(userId: userId));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -98,7 +98,7 @@ class _RoomGiftRankTabPageState
|
||||
// headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl,
|
||||
),
|
||||
onTap: () {
|
||||
showBottomInCenterDialog(
|
||||
showBottomInBottomDialog(
|
||||
navigatorKey.currentState!.context,
|
||||
RoomUserInfoCard(userId: userProfile?.id),
|
||||
);
|
||||
|
||||
1202
lib/modules/room/rocket/room_rocket_rank_page.dart
Normal file
@ -7,6 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/main.dart' show routeObserver;
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart';
|
||||
@ -51,7 +52,7 @@ class VoiceRoomPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with SingleTickerProviderStateMixin, RouteAware {
|
||||
static const int _chatTabCount = 3;
|
||||
static const Duration _luckyGiftComboWindow = Duration(seconds: 3);
|
||||
static const Duration _luckyGiftQueueDrainWindow = Duration(seconds: 3);
|
||||
@ -76,11 +77,17 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
int _shownRoomStartupFailureToken = 0;
|
||||
RtcProvider? _rtcProvider;
|
||||
bool _roomProviderRebuildScheduled = false;
|
||||
PageRoute<dynamic>? _routeObserverRoute;
|
||||
bool _roomRouteVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: _chatTabCount, vsync: this);
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).setVoiceRoomRouteVisible(true);
|
||||
_enableRoomVisualEffects();
|
||||
_tabController.addListener(_handleTabChange);
|
||||
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
|
||||
@ -110,11 +117,23 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
rtcProvider.addListener(_handleRoomProviderChanged);
|
||||
context.read<RoomMusicManager>().attachRtcProvider(rtcProvider);
|
||||
}
|
||||
final route = ModalRoute.of(context);
|
||||
if (route is PageRoute<dynamic> && _routeObserverRoute != route) {
|
||||
if (_routeObserverRoute != null) {
|
||||
routeObserver.unsubscribe(this);
|
||||
}
|
||||
_routeObserverRoute = route;
|
||||
routeObserver.subscribe(this, route);
|
||||
_setRoomRouteVisible(route.isCurrent);
|
||||
}
|
||||
_ensureRoomVisualEffectsEnabled();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
_routeObserverRoute = null;
|
||||
_setRoomRouteVisible(false);
|
||||
_rtcProvider?.removeListener(_handleRoomProviderChanged);
|
||||
_rtcProvider = null;
|
||||
_roomProviderRebuildScheduled = false;
|
||||
@ -125,6 +144,36 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
_setRoomRouteVisible(true);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
_setRoomRouteVisible(true);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPushNext() {
|
||||
_setRoomRouteVisible(false);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop() {
|
||||
_setRoomRouteVisible(false);
|
||||
}
|
||||
|
||||
void _setRoomRouteVisible(bool visible) {
|
||||
if (_roomRouteVisible == visible) {
|
||||
return;
|
||||
}
|
||||
_roomRouteVisible = visible;
|
||||
final rtcProvider =
|
||||
_rtcProvider ?? Provider.of<RtcProvider>(context, listen: false);
|
||||
rtcProvider.setVoiceRoomRouteVisible(visible);
|
||||
}
|
||||
|
||||
void _handleTabChange() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -160,6 +209,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
void _ensureRoomVisualEffectsEnabled() {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
if (rtcProvider.currenRoom == null ||
|
||||
!_roomRouteVisible ||
|
||||
rtcProvider.roomVisualEffectsEnabled) {
|
||||
return;
|
||||
}
|
||||
@ -168,6 +218,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
|
||||
void _suspendRoomVisualEffects() {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
|
||||
rtcProvider.setVoiceRoomRouteVisible(false);
|
||||
rtcProvider.setRoomVisualEffectsEnabled(false);
|
||||
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
@ -184,7 +237,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
_clearGiftFlightBatchDedupeTimers();
|
||||
_clearGiftVisualBatchDedupeTimers();
|
||||
_giftSeatFlightController.clear();
|
||||
OverlayManager().removeRoom();
|
||||
OverlayManager().removeRoom(roomId: currentRoomId);
|
||||
SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend');
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
unawaited(
|
||||
|
||||
@ -9,6 +9,7 @@ import 'package:yumi/modules/room/background/room_background_select_page.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_upload_page.dart';
|
||||
import 'package:yumi/modules/room/edit/room_edit_page.dart';
|
||||
import 'package:yumi/modules/room/music/room_music_page.dart';
|
||||
import 'package:yumi/modules/room/rocket/room_rocket_rank_page.dart';
|
||||
import 'package:yumi/modules/room/them/room_theme_page.dart';
|
||||
import 'package:yumi/modules/room/voice_room_page.dart';
|
||||
import 'package:yumi/app/routes/sc_router_init.dart';
|
||||
@ -28,6 +29,7 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
static String roomBackgroundPreview = '/room/background/preview';
|
||||
static String roomBackgroundUpload = '/room/background/upload';
|
||||
static String roomMusic = '/room/music';
|
||||
static String roomRocketRank = '/room/rocket/rank';
|
||||
static int _voiceRoomRouteDepth = 0;
|
||||
|
||||
static bool get isVoiceRoomOpen => _voiceRoomRouteDepth > 0;
|
||||
@ -79,13 +81,15 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
final floatingGiftListener = rtmProvider.msgFloatingGiftListener;
|
||||
final luckyGiftRewardTickerListener =
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener;
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
|
||||
|
||||
rtcProvider.setRoomVisualEffectsEnabled(false);
|
||||
rtmProvider.msgFloatingGiftListener = null;
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener = null;
|
||||
RoomEntranceHelper.clearQueue();
|
||||
giftAnimationManager.clearActiveAnimations();
|
||||
OverlayManager().removeRoom();
|
||||
OverlayManager().removeRoom(roomId: currentRoomId);
|
||||
SCRoomEffectScheduler().clearDeferredTasks(reason: 'room_background_route');
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
|
||||
@ -231,6 +235,33 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<T?> openRoomRocketRank<T>(
|
||||
BuildContext context, {
|
||||
String? roomId,
|
||||
int initialLevel = 0,
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
||||
_buildDarkRoute<T>(
|
||||
RoomRocketRankPage(roomId: roomId, initialLevel: initialLevel),
|
||||
settings: RouteSettings(name: roomRocketRank),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<T?> openRoomRocketRankOn<T>(
|
||||
NavigatorState navigator, {
|
||||
String? roomId,
|
||||
int initialLevel = 0,
|
||||
}) {
|
||||
return navigator.push<T>(
|
||||
_buildDarkRoute<T>(
|
||||
RoomRocketRankPage(roomId: roomId, initialLevel: initialLevel),
|
||||
settings: RouteSettings(name: roomRocketRank),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initRouter(FluroRouter router) {
|
||||
router.define(
|
||||
@ -263,6 +294,10 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
roomMusic,
|
||||
handler: Handler(handlerFunc: (_, params) => const RoomMusicPage()),
|
||||
);
|
||||
router.define(
|
||||
roomRocketRank,
|
||||
handler: Handler(handlerFunc: (_, params) => const RoomRocketRankPage()),
|
||||
);
|
||||
router.define(
|
||||
roomEdit,
|
||||
handler: Handler(
|
||||
|
||||
@ -535,17 +535,17 @@ class _SearchRoomListState extends State<SearchRoomList> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
(e.extValues?.roomSetting?.password?.isEmpty ?? false)
|
||||
e.extValues?.existsPassword != true
|
||||
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
|
||||
: Image.asset(
|
||||
"sc_images/index/sc_icon_room_suo.png",
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
),
|
||||
(e.extValues?.roomSetting?.password?.isEmpty ?? false)
|
||||
e.extValues?.existsPassword != true
|
||||
? SizedBox(width: 3.w)
|
||||
: Container(height: 10.w),
|
||||
(e.extValues?.roomSetting?.password?.isEmpty ?? false)
|
||||
e.extValues?.existsPassword != true
|
||||
? text(
|
||||
e.displayMemberCount,
|
||||
fontSize: 10.sp,
|
||||
|
||||
@ -448,13 +448,13 @@ class _SplashPageState extends State<SplashPage> {
|
||||
|
||||
Future<void> _goMainPage() async {
|
||||
try {
|
||||
await SCVersionUtils.checkReview();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
var user = AccountStorage().getCurrentUser();
|
||||
var token = AccountStorage().getToken();
|
||||
if (user != null && token.isNotEmpty) {
|
||||
final canContinue = await SCVersionUtils.checkReview(context: context);
|
||||
if (!mounted || !canContinue) {
|
||||
return;
|
||||
}
|
||||
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
|
||||
return;
|
||||
}
|
||||
@ -464,6 +464,10 @@ class _SplashPageState extends State<SplashPage> {
|
||||
}
|
||||
if (restoredUser != null &&
|
||||
(restoredUser.token ?? "").trim().isNotEmpty) {
|
||||
final canContinue = await SCVersionUtils.checkReview(context: context);
|
||||
if (!mounted || !canContinue) {
|
||||
return;
|
||||
}
|
||||
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
|
||||
} else {
|
||||
SCNavigatorUtils.pushLoginIfNeeded(context, replace: true);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
@ -50,6 +51,7 @@ class _MePage2State extends State<MePage2> {
|
||||
listen: false,
|
||||
);
|
||||
profileManager.fetchUserProfileData();
|
||||
profileManager.getUserIdentity();
|
||||
profileManager.balance();
|
||||
_loadCounter();
|
||||
_loadVipStatus();
|
||||
@ -660,6 +662,16 @@ class _MePage2State extends State<MePage2> {
|
||||
final items = <_MenuRowData>[];
|
||||
final hasCurrentUser = userProfile.currentUserProfile != null;
|
||||
final isAgent = userProfile.userIdentity?.agent ?? false;
|
||||
final isYumiManager = userProfile.userIdentity?.yumiManager ?? false;
|
||||
if (kDebugMode) {
|
||||
final currentProfile = userProfile.currentUserProfile;
|
||||
debugPrint(
|
||||
'[MePage2] menu identity userId=${currentProfile?.id}, '
|
||||
'account=${currentProfile?.account}, hasCurrentUser=$hasCurrentUser, '
|
||||
'agent=${userProfile.userIdentity?.agent}, admin=${userProfile.userIdentity?.admin}, '
|
||||
'manager=${userProfile.userIdentity?.manager}, yumiManager=${userProfile.userIdentity?.yumiManager}',
|
||||
);
|
||||
}
|
||||
|
||||
// 1. 主播/代理入口
|
||||
if (hasCurrentUser) {
|
||||
@ -754,6 +766,25 @@ class _MePage2State extends State<MePage2> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isYumiManager) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[MePage2] show Manager Center');
|
||||
}
|
||||
items.add(
|
||||
_MenuRowData(
|
||||
title: SCAppLocalizations.of(context)!.managerCenter,
|
||||
assetIcon: 'sc_images/index/sc_icon_admin_center.png',
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.managerCenterUrl)}&showTitle=false",
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (kDebugMode) {
|
||||
debugPrint('[MePage2] hide Manager Center because yumiManager=$isYumiManager');
|
||||
}
|
||||
return _buildMenuCard(items);
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart';
|
||||
|
||||
import '../../../app/constants/sc_screen.dart';
|
||||
import '../../../shared/business_logic/models/res/sc_user_counter_res.dart';
|
||||
@ -393,13 +394,20 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderBadgePlaceholder() {
|
||||
return SizedBox(
|
||||
height: 24.w,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [],
|
||||
),
|
||||
Widget _buildHeaderBadgeStrip(SocialChatUserProfileManager ref) {
|
||||
return SCUserBadgeStrip(
|
||||
userId: ref.userProfile?.id ?? widget.tageId,
|
||||
fallbackBadges:
|
||||
ref.userProfile?.wearBadge
|
||||
?.where((item) => item.use ?? false)
|
||||
.toList() ??
|
||||
const <WearBadge>[],
|
||||
height: 26.w,
|
||||
badgeHeight: 24.w,
|
||||
longBadgeWidth: 58.w,
|
||||
spacing: 6.w,
|
||||
maxBadges: 8,
|
||||
reserveSpace: true,
|
||||
);
|
||||
}
|
||||
|
||||
@ -612,7 +620,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
SizedBox(height: 5.w),
|
||||
_buildHeaderMeta(ref),
|
||||
SizedBox(height: 4.w),
|
||||
_buildHeaderBadgePlaceholder(),
|
||||
_buildHeaderBadgeStrip(ref),
|
||||
SizedBox(height: 6.w),
|
||||
_buildHeaderIntroduction(ref),
|
||||
SizedBox(height: 12.w),
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_svga/flutter_svga.dart';
|
||||
@ -564,7 +566,6 @@ class _GiftWallSvgaEffectOverlay extends StatefulWidget {
|
||||
class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final SVGAAnimationController _controller;
|
||||
MovieEntity? _retainedMovieEntity;
|
||||
bool _loaded = false;
|
||||
bool _finished = false;
|
||||
|
||||
@ -584,15 +585,9 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
|
||||
Future<void> _loadAndPlay() async {
|
||||
try {
|
||||
final movieEntity = await SCGiftVapSvgaManager().acquireSvgaEntity(
|
||||
widget.sourceUrl,
|
||||
);
|
||||
if (!mounted || _finished) {
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
|
||||
return;
|
||||
}
|
||||
final movieEntity = await _loadMovieEntity(widget.sourceUrl);
|
||||
if (!mounted || _finished) return;
|
||||
setState(() {
|
||||
_retainedMovieEntity = movieEntity;
|
||||
_loaded = true;
|
||||
_controller.videoItem = movieEntity;
|
||||
});
|
||||
@ -604,6 +599,28 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
}
|
||||
}
|
||||
|
||||
Future<MovieEntity> _loadMovieEntity(String sourceUrl) async {
|
||||
final cache = SCGiftVapSvgaManager().videoItemCache;
|
||||
final cached = cache[sourceUrl];
|
||||
if (cached != null) return cached;
|
||||
|
||||
final pathType = SCPathUtils.getPathType(sourceUrl);
|
||||
late final MovieEntity movieEntity;
|
||||
if (pathType == PathType.network) {
|
||||
movieEntity = await SVGAParser.shared.decodeFromURL(sourceUrl);
|
||||
} else if (pathType == PathType.asset) {
|
||||
movieEntity = await SVGAParser.shared.decodeFromAssets(sourceUrl);
|
||||
} else if (pathType == PathType.file) {
|
||||
final bytes = await File(sourceUrl).readAsBytes();
|
||||
movieEntity = await SVGAParser.shared.decodeFromBuffer(bytes);
|
||||
} else {
|
||||
throw Exception("Unsupported SVGA source: $sourceUrl");
|
||||
}
|
||||
movieEntity.autorelease = false;
|
||||
cache[sourceUrl] = movieEntity;
|
||||
return movieEntity;
|
||||
}
|
||||
|
||||
void _finish() {
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
@ -614,8 +631,6 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
void dispose() {
|
||||
_controller.removeStatusListener(_handleStatusChanged);
|
||||
_controller.dispose();
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(_retainedMovieEntity);
|
||||
_retainedMovieEntity = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@ -48,6 +49,9 @@ class _WebViewPageState extends State<WebViewPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
urlLink = SCH5UrlUtils.appendAppParams(widget.url);
|
||||
if (kDebugMode) {
|
||||
debugPrint('[WebViewPage] load url=${_maskSensitiveUrl(urlLink)}');
|
||||
}
|
||||
// 初始化 WebViewController
|
||||
_controller =
|
||||
WebViewController()
|
||||
@ -138,11 +142,21 @@ class _WebViewPageState extends State<WebViewPage> {
|
||||
});
|
||||
},
|
||||
onPageStarted: (String url) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[WebViewPage] page started url=${_maskSensitiveUrl(url)}',
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
},
|
||||
onPageFinished: (String url) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[WebViewPage] page finished url=${_maskSensitiveUrl(url)}',
|
||||
);
|
||||
}
|
||||
// 延迟500毫秒注入,等待Vue3初始化
|
||||
Future.delayed(Duration(milliseconds: 550), () {
|
||||
_injectJavaScriptInterface();
|
||||
@ -310,6 +324,23 @@ class _WebViewPageState extends State<WebViewPage> {
|
||||
}
|
||||
|
||||
void _onWebResourceError(WebResourceError error) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[WebViewPage] resource error code=${error.errorCode}, '
|
||||
'type=${error.errorType}, url=${_maskSensitiveUrl(error.url ?? '')}, '
|
||||
'description=${error.description}',
|
||||
);
|
||||
}
|
||||
// 可以在这里添加错误处理逻辑,比如显示错误页面
|
||||
}
|
||||
|
||||
String _maskSensitiveUrl(String url) {
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null) return url;
|
||||
final params = Map<String, String>.from(uri.queryParameters);
|
||||
if (params.containsKey('token')) {
|
||||
params['token'] = '***';
|
||||
}
|
||||
return uri.replace(queryParameters: params).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/shared/tools/sc_permission_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
@ -39,6 +40,7 @@ import 'package:yumi/shared/business_logic/models/res/room_res.dart'
|
||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_password_dialog.dart';
|
||||
import '../../shared/tools/sc_heartbeat_utils.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart';
|
||||
@ -51,6 +53,8 @@ import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_vip_res.dart';
|
||||
import '../../shared/tools/sc_room_profile_cache.dart';
|
||||
import '../../ui_kit/components/sc_float_ichart.dart';
|
||||
import '../../ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart';
|
||||
import '../../ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart';
|
||||
|
||||
typedef OnSoundVoiceChange = Function(num index, int volum);
|
||||
typedef RtcProvider = RealTimeCommunicationManager;
|
||||
@ -117,6 +121,7 @@ class RoomEntryPreviewData {
|
||||
this.updateTime,
|
||||
this.activeTime,
|
||||
this.roomSetting,
|
||||
this.existsPassword,
|
||||
this.ownerProfile,
|
||||
this.roomAdminCount,
|
||||
this.roomMemberCount,
|
||||
@ -138,6 +143,7 @@ class RoomEntryPreviewData {
|
||||
countryName: room.countryName,
|
||||
nationalFlag: room.nationalFlag,
|
||||
roomSetting: room.extValues?.roomSetting,
|
||||
existsPassword: room.extValues?.existsPassword == true,
|
||||
ownerProfile: room.userProfile,
|
||||
roomAdminCount: room.roomCounter?.adminCount,
|
||||
roomMemberCount: room.roomCounter?.memberCount,
|
||||
@ -160,6 +166,7 @@ class RoomEntryPreviewData {
|
||||
countryName: roomProfile?.countryName,
|
||||
nationalFlag: roomProfile?.nationalFlag,
|
||||
roomSetting: roomProfile?.extValues?.roomSetting,
|
||||
existsPassword: roomProfile?.extValues?.existsPassword == true,
|
||||
ownerProfile: roomProfile?.userProfile,
|
||||
roomAdminCount: roomProfile?.roomCounter?.adminCount,
|
||||
roomMemberCount: roomProfile?.roomCounter?.memberCount,
|
||||
@ -189,6 +196,7 @@ class RoomEntryPreviewData {
|
||||
updateTime: room.updateTime,
|
||||
activeTime: room.activeTime,
|
||||
roomSetting: room.setting,
|
||||
existsPassword: (room.setting?.password ?? "").trim().isNotEmpty,
|
||||
ownerProfile: ownerProfile,
|
||||
roomAdminCount: room.counter?.adminCount,
|
||||
roomMemberCount: room.counter?.memberCount,
|
||||
@ -214,6 +222,7 @@ class RoomEntryPreviewData {
|
||||
final num? updateTime;
|
||||
final num? activeTime;
|
||||
final RoomSetting? roomSetting;
|
||||
final bool? existsPassword;
|
||||
final SocialChatUserProfile? ownerProfile;
|
||||
final num? roomAdminCount;
|
||||
final num? roomMemberCount;
|
||||
@ -288,6 +297,11 @@ class _PendingRoomStartupSeatAction {
|
||||
}
|
||||
|
||||
class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
|
||||
static const String _roomRocketPagLaunchDialogTag =
|
||||
'showRoomRocketPagLaunchDialog';
|
||||
static const int _roomPasswordNotTrueErrorCode = 9005;
|
||||
|
||||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||||
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
|
||||
static const int _exitNetworkRetryLimit = 3;
|
||||
@ -311,6 +325,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
bool needUpDataUserInfo = false;
|
||||
bool _roomVisualEffectsEnabled = false;
|
||||
bool _voiceRoomRouteVisible = false;
|
||||
bool _isExitingCurrentVoiceRoomSession = false;
|
||||
bool _isHandlingAgoraRoomFailure = false;
|
||||
bool _agoraJoined = false;
|
||||
@ -329,6 +344,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Timer? _joinAgoraTimeoutTimer;
|
||||
Timer? _agoraDisconnectedCleanupTimer;
|
||||
Timer? _roomEntryEffectTimer;
|
||||
Timer? _roomRocketLaunchAnimationTimer;
|
||||
Timer? _roomRedPacketPresenceTimer;
|
||||
Completer<void>? _joinAgoraCompleter;
|
||||
Future<RtcEngine>? _rtcEngineInitTask;
|
||||
@ -435,6 +451,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled;
|
||||
|
||||
bool get isVoiceRoomRouteVisible => _voiceRoomRouteVisible;
|
||||
|
||||
bool get isExitingCurrentVoiceRoomSession =>
|
||||
_isExitingCurrentVoiceRoomSession;
|
||||
|
||||
@ -474,11 +492,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (!enabled) {
|
||||
_roomEntryEffectTimer?.cancel();
|
||||
_roomEntryEffectTimer = null;
|
||||
_roomRocketLaunchAnimationTimer?.cancel();
|
||||
_roomRocketLaunchAnimationTimer = null;
|
||||
}
|
||||
_roomVisualEffectsEnabled = enabled;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVoiceRoomRouteVisible(bool visible) {
|
||||
_voiceRoomRouteVisible = visible;
|
||||
}
|
||||
|
||||
void _setRoomStartupLoading() {
|
||||
_isHandlingRoomStartupFailure = false;
|
||||
_roomStartupFailureType = RoomStartupFailureType.none;
|
||||
@ -2107,6 +2131,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
SCTts.show('Microphone permission is denied.');
|
||||
throw ArgumentError('Microphone permission is denied.');
|
||||
}
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
if (roomId == currenRoom?.roomProfile?.roomProfile?.id) {
|
||||
if (_currentRoomIsEntryPreview ||
|
||||
_roomStartupStatus == RoomStartupStatus.loading) {
|
||||
@ -2149,6 +2176,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_startVoiceRoomForegroundService();
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
} else {
|
||||
JoinRoomRes? preEnteredRoom;
|
||||
if (pwd == null && _previewRoomRequiresPassword(context, previewData)) {
|
||||
preEnteredRoom = await _showLockedRoomEntryDialog(
|
||||
roomId,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
redPackId: redPackId,
|
||||
);
|
||||
if (preEnteredRoom == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currenRoom != null) {
|
||||
_switchAwayFromCurrentRoomForNewEntry();
|
||||
}
|
||||
@ -2171,15 +2209,26 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
notifyListeners();
|
||||
final entryRequestSerial = ++_roomEntryRequestSerial;
|
||||
unawaited(
|
||||
_enterAndInitializeVoiceRoomSession(
|
||||
roomId,
|
||||
entryRequestSerial: entryRequestSerial,
|
||||
pwd: pwd,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
redPackId: redPackId,
|
||||
),
|
||||
);
|
||||
if (preEnteredRoom != null) {
|
||||
unawaited(
|
||||
_completeEnteredVoiceRoomSession(
|
||||
preEnteredRoom,
|
||||
entryRequestSerial: entryRequestSerial,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
redPackId: redPackId,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
_enterAndInitializeVoiceRoomSession(
|
||||
roomId,
|
||||
entryRequestSerial: entryRequestSerial,
|
||||
pwd: pwd,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
redPackId: redPackId,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2197,32 +2246,167 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
redPackId: redPackId,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
);
|
||||
if (!_isActiveRoomEntryRequest(entryRequestSerial)) {
|
||||
unawaited(_cleanupStaleEnteredRoom(enteredRoom));
|
||||
return;
|
||||
}
|
||||
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
|
||||
_syncCurrentUserVipHintFromRoom(currenRoom);
|
||||
_startVoiceRoomForegroundService();
|
||||
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
|
||||
_currentRoomIsEntryPreview = false;
|
||||
_previewRoomSeatCount = null;
|
||||
notifyListeners();
|
||||
await initializeRoomSession(
|
||||
await _completeEnteredVoiceRoomSession(
|
||||
enteredRoom,
|
||||
entryRequestSerial: entryRequestSerial,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
redPackId: redPackId,
|
||||
shouldOpenRoomPage: false,
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
} catch (e, _) {
|
||||
if (!_isActiveRoomEntryRequest(entryRequestSerial)) {
|
||||
return;
|
||||
}
|
||||
if (pwd == null && _isRoomPasswordError(e)) {
|
||||
final enteredRoom = await _showLockedRoomEntryDialog(
|
||||
roomId,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
redPackId: redPackId,
|
||||
);
|
||||
if (enteredRoom != null &&
|
||||
_isActiveRoomEntryRequest(entryRequestSerial)) {
|
||||
await _completeEnteredVoiceRoomSession(
|
||||
enteredRoom,
|
||||
entryRequestSerial: entryRequestSerial,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
redPackId: redPackId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_stopVoiceRoomForegroundService();
|
||||
_setRoomStartupFailed(RoomStartupFailureType.entry);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _completeEnteredVoiceRoomSession(
|
||||
JoinRoomRes enteredRoom, {
|
||||
required int entryRequestSerial,
|
||||
bool needOpenRedenvelope = false,
|
||||
String redPackId = "",
|
||||
}) async {
|
||||
if (!_isActiveRoomEntryRequest(entryRequestSerial)) {
|
||||
unawaited(_cleanupStaleEnteredRoom(enteredRoom));
|
||||
return;
|
||||
}
|
||||
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
|
||||
_syncCurrentUserVipHintFromRoom(currenRoom);
|
||||
_startVoiceRoomForegroundService();
|
||||
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
|
||||
_currentRoomIsEntryPreview = false;
|
||||
_previewRoomSeatCount = null;
|
||||
notifyListeners();
|
||||
await initializeRoomSession(
|
||||
entryRequestSerial: entryRequestSerial,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
redPackId: redPackId,
|
||||
shouldOpenRoomPage: false,
|
||||
);
|
||||
}
|
||||
|
||||
bool _previewRoomRequiresPassword(
|
||||
BuildContext context,
|
||||
RoomEntryPreviewData? previewData,
|
||||
) {
|
||||
if (previewData?.existsPassword != true) {
|
||||
return false;
|
||||
}
|
||||
final ownerId = (previewData?.userId ?? "").trim();
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (ownerId.isNotEmpty && ownerId == currentUserId) {
|
||||
return false;
|
||||
}
|
||||
final userIdentity =
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).userIdentity;
|
||||
if (userIdentity?.superAdmin == true ||
|
||||
userIdentity?.admin == true ||
|
||||
userIdentity?.yumiManager == true) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<JoinRoomRes?> _showLockedRoomEntryDialog(
|
||||
String roomId, {
|
||||
bool needOpenRedenvelope = false,
|
||||
String redPackId = "",
|
||||
}) async {
|
||||
JoinRoomRes? enteredRoom;
|
||||
await RoomPasswordDialog.show(
|
||||
onSubmit: (password) async {
|
||||
try {
|
||||
enteredRoom = await SCAccountRepository().entryRoom(
|
||||
roomId,
|
||||
pwd: password,
|
||||
redPackId: redPackId,
|
||||
needOpenRedenvelope: needOpenRedenvelope,
|
||||
silentErrorToast: true,
|
||||
);
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (_isRoomPasswordError(error)) {
|
||||
return "Password is incorrect";
|
||||
}
|
||||
final message = _readNetworkErrorMessage(error);
|
||||
return message.isNotEmpty ? message : "Failed to enter room";
|
||||
}
|
||||
},
|
||||
);
|
||||
return enteredRoom;
|
||||
}
|
||||
|
||||
bool _isRoomPasswordError(Object error) {
|
||||
return _readNetworkErrorCode(error) == _roomPasswordNotTrueErrorCode ||
|
||||
_readNetworkErrorCodeName(error) == "PASSWORD_NOT_TRUE";
|
||||
}
|
||||
|
||||
int? _readNetworkErrorCode(Object error) {
|
||||
if (error is DioException) {
|
||||
final responseData = error.response?.data;
|
||||
if (responseData is Map) {
|
||||
final value = responseData["errorCode"] ?? responseData["code"];
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
if (value is String) {
|
||||
return int.tryParse(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _readNetworkErrorCodeName(Object error) {
|
||||
if (error is DioException) {
|
||||
final responseData = error.response?.data;
|
||||
if (responseData is Map) {
|
||||
return (responseData["errorCodeName"] ?? responseData["code"] ?? "")
|
||||
.toString()
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
String _readNetworkErrorMessage(Object error) {
|
||||
if (error is DioException) {
|
||||
final responseData = error.response?.data;
|
||||
if (responseData is Map) {
|
||||
return (responseData["errorMsg"] ?? responseData["message"] ?? "")
|
||||
.toString()
|
||||
.trim();
|
||||
}
|
||||
return (error.error ?? error.message ?? "").toString().trim();
|
||||
}
|
||||
return error.toString().trim();
|
||||
}
|
||||
|
||||
JoinRoomRes _mergeVoiceRoomVipHints(
|
||||
JoinRoomRes enteredRoom,
|
||||
JoinRoomRes? previewRoom,
|
||||
@ -2857,29 +3041,186 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _loadRoomSecondaryData() {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
SCChatRoomRepository().rocketClaim(roomId).catchError((e) {
|
||||
return true; // 错误已处理
|
||||
});
|
||||
SCChatRoomRepository()
|
||||
.rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "")
|
||||
.catchError((e) {
|
||||
return true; // 错误已处理
|
||||
});
|
||||
SCChatRoomRepository()
|
||||
.rocketStatus(currenRoom?.roomProfile?.roomProfile?.id ?? "")
|
||||
.rocketStatus(roomId)
|
||||
.then((res) {
|
||||
roomRocketStatus = res;
|
||||
notifyListeners();
|
||||
})
|
||||
.catchError((e) {});
|
||||
_loadRoomRocketRewardPopups(roomId);
|
||||
_refreshRoomRedPacketListAfterEntry();
|
||||
|
||||
fetchRoomTaskClaimableCount();
|
||||
}
|
||||
|
||||
void _loadRoomRocketRewardPopups(String roomId) {
|
||||
if (roomId.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
SCChatRoomRepository()
|
||||
.roomRocketRewardPopups(roomId)
|
||||
.then((res) {
|
||||
if (res.records.isEmpty ||
|
||||
roomId !=
|
||||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
|
||||
return;
|
||||
}
|
||||
final recordIds = res.records
|
||||
.map((item) => item.id)
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
SmartDialog.show(
|
||||
tag: _roomRocketRewardDialogTag,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => RoomRocketRewardDialogLoader(
|
||||
roomId: roomId,
|
||||
initialPopupRecords: res.records,
|
||||
onClose: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
if (recordIds.isNotEmpty) {
|
||||
unawaited(
|
||||
SCChatRoomRepository().ackRoomRocketRewardPopups(
|
||||
recordIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
})
|
||||
.catchError((_) {});
|
||||
}
|
||||
|
||||
///更新房间火箭信息
|
||||
void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) {
|
||||
roomRocketStatus = res;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///刷新房间火箭信息
|
||||
Future<void> refreshRoomRocketStatus({String? roomId}) async {
|
||||
final resolvedRoomId =
|
||||
(roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
if (resolvedRoomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final res = await SCChatRoomRepository().rocketStatus(resolvedRoomId);
|
||||
if (resolvedRoomId !=
|
||||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
|
||||
return;
|
||||
}
|
||||
roomRocketStatus = res;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void handleRoomRocketLaunchBroadcast(
|
||||
RoomRocketLaunchBroadcastMessage launch,
|
||||
) {
|
||||
final currentRoomId =
|
||||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
if (!launch.isValid || launch.roomId != currentRoomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
_roomRocketLaunchAnimationTimer?.cancel();
|
||||
|
||||
if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) {
|
||||
final animationUrl =
|
||||
_firstNonBlankText([
|
||||
launch.rocketAnimationUrl,
|
||||
_rocketLaunchAnimationUrlForLevel(launch.safeLevel),
|
||||
]) ??
|
||||
"";
|
||||
if (animationUrl.isNotEmpty) {
|
||||
_playRoomRocketLaunchAnimation(
|
||||
animationUrl: animationUrl,
|
||||
launch: launch,
|
||||
);
|
||||
}
|
||||
_roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () {
|
||||
_roomRocketLaunchAnimationTimer = null;
|
||||
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
|
||||
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
|
||||
}
|
||||
|
||||
String? _rocketLaunchAnimationUrlForLevel(int level) {
|
||||
final levels = roomRocketStatus?.levels ?? const [];
|
||||
for (final item in levels) {
|
||||
if (item.level == level && item.rocketAnimationUrl.trim().isNotEmpty) {
|
||||
return item.rocketAnimationUrl;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _playRoomRocketLaunchAnimation({
|
||||
required String animationUrl,
|
||||
required RoomRocketLaunchBroadcastMessage launch,
|
||||
}) {
|
||||
if (RoomRocketPagEffectOverlay.isPag(animationUrl)) {
|
||||
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
|
||||
SmartDialog.show(
|
||||
tag: _roomRocketPagLaunchDialogTag,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => RoomRocketPagEffectOverlay(
|
||||
resource: animationUrl,
|
||||
topAvatarUrl: _roomRocketTop1AvatarUrl(),
|
||||
onCompleted: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
|
||||
_roomRocketLaunchAnimationTimer?.cancel();
|
||||
_roomRocketLaunchAnimationTimer = null;
|
||||
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
SCGiftVapSvgaManager().play(
|
||||
animationUrl,
|
||||
priority: SCGiftVapSvgaManager.entryEffectPriority + 100,
|
||||
type: SCGiftVapSvgaManager.entryEffectType,
|
||||
);
|
||||
}
|
||||
|
||||
String? _roomRocketTop1AvatarUrl() {
|
||||
final kings = roomRocketStatus?.rocketKings ?? const [];
|
||||
if (kings.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
for (final item in kings) {
|
||||
if (item.rank == 1 && item.userAvatar.trim().isNotEmpty) {
|
||||
return item.userAvatar;
|
||||
}
|
||||
}
|
||||
for (final item in kings) {
|
||||
if (item.userAvatar.trim().isNotEmpty) {
|
||||
return item.userAvatar;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
///获取在线用户
|
||||
Future<void> fetchOnlineUsersList({bool notifyIfUnchanged = true}) async {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
@ -3345,6 +3686,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_stopVoiceRoomForegroundService();
|
||||
_roomEntryEffectTimer?.cancel();
|
||||
_roomEntryEffectTimer = null;
|
||||
_roomRocketLaunchAnimationTimer?.cancel();
|
||||
_roomRocketLaunchAnimationTimer = null;
|
||||
_stopRoomRedPacketPresenceHeartbeat();
|
||||
_previewRoomSeatCount = null;
|
||||
_finishRoomStartupSeatLoading(executePendingAction: false);
|
||||
@ -3383,6 +3726,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
redPacketList.clear();
|
||||
currenRoom = null;
|
||||
_roomVisualEffectsEnabled = false;
|
||||
_voiceRoomRouteVisible = false;
|
||||
_isExitingCurrentVoiceRoomSession = false;
|
||||
isMic = true;
|
||||
isMusicPlaying = false;
|
||||
@ -3432,7 +3776,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (clickUser.id ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
///是自己,直接打开资料卡
|
||||
showBottomInCenterDialog(
|
||||
showBottomInBottomDialog(
|
||||
context!,
|
||||
RoomUserInfoCard(userId: clickUser.id),
|
||||
);
|
||||
@ -3542,7 +3886,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (normalizedUserId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
showBottomInCenterDialog(
|
||||
showBottomInBottomDialog(
|
||||
context!,
|
||||
RoomUserInfoCard(userId: normalizedUserId),
|
||||
);
|
||||
|
||||
@ -47,18 +47,21 @@ import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart'
|
||||
hide Data;
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_pending_cache.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/services/gift/gift_system_manager.dart';
|
||||
@ -103,6 +106,8 @@ class _LuckGiftPushQueueEntry {
|
||||
}
|
||||
|
||||
class RealTimeMessagingManager extends ChangeNotifier {
|
||||
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
|
||||
|
||||
static const int _giftComboMergeWindowMs = 3000;
|
||||
static const int _maxLuckGiftPushQueueLength = 12;
|
||||
static const int _luckyGiftFloatMinMultiple = 5;
|
||||
@ -114,6 +119,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
static const int _luckyGiftBurstPlaybackWatchdogMs = 8000;
|
||||
|
||||
BuildContext? context;
|
||||
final Set<String> _sentRoomRocketRewardMessageKeys = <String>{};
|
||||
|
||||
void _giftFxLog(String message) {}
|
||||
|
||||
@ -1098,6 +1104,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return true;
|
||||
case SCRoomMsgType.roomRedPacketClaim:
|
||||
return true;
|
||||
case SCRoomMsgType.rocketRewardUser:
|
||||
return true;
|
||||
case SCRoomMsgType.emoticons:
|
||||
return (msg.number ?? -1) < 0;
|
||||
default:
|
||||
@ -1127,6 +1135,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_hasRoomRedPacketClaimMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
if (msg.type == SCRoomMsgType.rocketRewardUser &&
|
||||
_hasRoomRocketRewardMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
roomAllMsgList.insert(0, msg);
|
||||
if (roomAllMsgList.length > 250) {
|
||||
@ -1159,6 +1171,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
|
||||
}
|
||||
msgChatListener?.call(msg);
|
||||
} else if (msg.type == SCRoomMsgType.rocketRewardUser) {
|
||||
roomChatMsgList.insert(0, msg);
|
||||
if (roomChatMsgList.length > 250) {
|
||||
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
|
||||
}
|
||||
msgChatListener?.call(msg);
|
||||
} else if (msg.type == SCRoomMsgType.gift ||
|
||||
msg.type == SCRoomMsgType.luckGiftAnimOther) {
|
||||
roomGiftMsgList.insert(0, msg);
|
||||
@ -1266,6 +1284,32 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return ['legacy', userId, msg.msg ?? '', amount, time].join('|');
|
||||
}
|
||||
|
||||
bool _hasRoomRocketRewardMessage(Msg msg) {
|
||||
final key = _roomRocketRewardMessageKey(msg);
|
||||
if (key.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return roomAllMsgList.any(
|
||||
(item) =>
|
||||
item.type == SCRoomMsgType.rocketRewardUser &&
|
||||
_roomRocketRewardMessageKey(item) == key,
|
||||
);
|
||||
}
|
||||
|
||||
String _roomRocketRewardMessageKey(Msg msg) {
|
||||
final stableKey = (msg.giftBatchId ?? '').trim();
|
||||
if (stableKey.isNotEmpty) {
|
||||
return stableKey;
|
||||
}
|
||||
return [
|
||||
msg.groupId ?? '',
|
||||
msg.user?.id ?? '',
|
||||
msg.number?.toString() ?? '',
|
||||
msg.role ?? '',
|
||||
msg.msg ?? '',
|
||||
].join('|');
|
||||
}
|
||||
|
||||
Msg? _mergeGiftMessageIfNeeded(Msg incoming) {
|
||||
if (incoming.type != SCRoomMsgType.gift &&
|
||||
incoming.type != SCRoomMsgType.luckGiftAnimOther) {
|
||||
@ -1697,6 +1741,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
|
||||
return rtcProvider.shouldShowRoomVisualEffects &&
|
||||
rtcProvider.isVoiceRoomRouteVisible &&
|
||||
currentRoomId.isNotEmpty &&
|
||||
currentRoomId == normalizedRoomId;
|
||||
}
|
||||
@ -1784,6 +1829,370 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
void _handleVoiceRoomRocketStatusUpdate(dynamic payload) {
|
||||
final data = _broadcastPayloadMap(payload);
|
||||
final roomId = _payloadText(data['roomId']);
|
||||
if (!_isCurrentVoiceRoom(roomId)) {
|
||||
return;
|
||||
}
|
||||
final currentContext = context;
|
||||
if (currentContext == null || !currentContext.mounted) {
|
||||
return;
|
||||
}
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
currentContext,
|
||||
listen: false,
|
||||
);
|
||||
unawaited(rtcProvider.refreshRoomRocketStatus(roomId: roomId));
|
||||
}
|
||||
|
||||
void _handleVoiceRoomRocketLaunchBroadcast(dynamic payload) {
|
||||
final launch = RoomRocketLaunchBroadcastMessage.fromJson(
|
||||
_broadcastPayloadMap(payload),
|
||||
);
|
||||
if (!launch.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentContext = context;
|
||||
if (currentContext != null && currentContext.mounted) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
currentContext,
|
||||
listen: false,
|
||||
);
|
||||
final currentRoomId =
|
||||
(rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim();
|
||||
if (currentRoomId == launch.roomId) {
|
||||
rtcProvider.handleRoomRocketLaunchBroadcast(launch);
|
||||
}
|
||||
}
|
||||
|
||||
OverlayManager().addMessage(_buildRoomRocketLaunchFloatingMessage(launch));
|
||||
}
|
||||
|
||||
void _handleVoiceRoomRocketRewardPopup(dynamic payload) {
|
||||
final data = _broadcastPayloadMap(payload);
|
||||
final roomId = _payloadText(data['roomId']);
|
||||
final currentRoomId = _currentVoiceRoomId();
|
||||
final resolvedRoomId = roomId.isEmpty ? currentRoomId : roomId;
|
||||
if (resolvedRoomId.isEmpty ||
|
||||
(currentRoomId.isNotEmpty && resolvedRoomId != currentRoomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final userIds = _payloadStringList(data['userIds']);
|
||||
final currentUserId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? '';
|
||||
if (currentUserId.isEmpty || !userIds.contains(currentUserId)) {
|
||||
return;
|
||||
}
|
||||
unawaited(_showRoomRocketRewardPopupDialog(resolvedRoomId));
|
||||
}
|
||||
|
||||
Future<void> _sendRoomRocketRewardPublicMessages(
|
||||
String roomId,
|
||||
Map<String, dynamic> payload,
|
||||
) async {
|
||||
final groupId = _currentVoiceRoomGroupId();
|
||||
if (groupId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final launchNo = _payloadText(payload['launchNo']);
|
||||
var records = _rocketRewardRecordsFromPayload(payload);
|
||||
if (records.isEmpty) {
|
||||
records = await _loadRocketRewardPublicRecords(roomId, launchNo);
|
||||
}
|
||||
if (records.isNotEmpty) {
|
||||
for (final record in records) {
|
||||
await _sendRoomRocketRewardMessage(
|
||||
groupId,
|
||||
_buildRocketRewardRecordMsg(groupId, record),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final userIds = _payloadStringList(payload['userIds']);
|
||||
for (final userId in userIds) {
|
||||
await _sendRoomRocketRewardMessage(
|
||||
groupId,
|
||||
_buildRocketRewardFallbackMsg(groupId, payload, userId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendRoomRocketRewardMessage(String groupId, Msg msg) async {
|
||||
final key = _roomRocketRewardMessageKey(msg);
|
||||
if (key.isEmpty || !_sentRoomRocketRewardMessageKeys.add(key)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final textMsg = await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.createCustomMessage(data: jsonEncode(msg.toJson()));
|
||||
if (textMsg.code != 0 || textMsg.data?.id == null) {
|
||||
_sentRoomRocketRewardMessageKeys.remove(key);
|
||||
return;
|
||||
}
|
||||
final sendResult = await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.sendMessage(id: textMsg.data!.id!, groupID: groupId, receiver: '');
|
||||
if (sendResult.code == 0) {
|
||||
addMsg(msg);
|
||||
} else {
|
||||
_sentRoomRocketRewardMessageKeys.remove(key);
|
||||
}
|
||||
} catch (_) {
|
||||
_sentRoomRocketRewardMessageKeys.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SCRoomRocketRewardRecordRes>> _loadRocketRewardPublicRecords(
|
||||
String roomId,
|
||||
String launchNo,
|
||||
) async {
|
||||
if (roomId.trim().isEmpty || launchNo.trim().isEmpty) {
|
||||
return const <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
try {
|
||||
final res = await SCChatRoomRepository().roomRocketRewardRecords(
|
||||
roomId,
|
||||
cursor: 1,
|
||||
limit: 100,
|
||||
);
|
||||
return res.records
|
||||
.where((item) => item.launchNo.trim() == launchNo.trim())
|
||||
.toList(growable: false);
|
||||
} catch (_) {
|
||||
return const <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
}
|
||||
|
||||
List<SCRoomRocketRewardRecordRes> _rocketRewardRecordsFromPayload(
|
||||
Map<String, dynamic> payload,
|
||||
) {
|
||||
final candidates = [
|
||||
payload['records'],
|
||||
payload['rewardRecords'],
|
||||
payload['rewards'],
|
||||
_broadcastPayloadMap(payload['body'])['records'],
|
||||
_broadcastPayloadMap(payload['data'])['records'],
|
||||
];
|
||||
for (final candidate in candidates) {
|
||||
if (candidate is Iterable) {
|
||||
final records = candidate
|
||||
.map(SCRoomRocketRewardRecordRes.fromJson)
|
||||
.toList(growable: false);
|
||||
final usableRecords = records
|
||||
.where((item) => item.userId.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (usableRecords.isNotEmpty) {
|
||||
return usableRecords;
|
||||
}
|
||||
}
|
||||
}
|
||||
return const <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
|
||||
Msg _buildRocketRewardRecordMsg(
|
||||
String groupId,
|
||||
SCRoomRocketRewardRecordRes record,
|
||||
) {
|
||||
return Msg(
|
||||
groupId: groupId,
|
||||
msg: record.rewardCover,
|
||||
type: SCRoomMsgType.rocketRewardUser,
|
||||
user: SocialChatUserProfile(
|
||||
id: record.userId,
|
||||
userNickname: record.displayName,
|
||||
userAvatar: record.userAvatar,
|
||||
account: record.account,
|
||||
),
|
||||
role: _rocketRewardRole(record.rewardType, record.rewardCover),
|
||||
number: record.level <= 0 ? 1 : record.level,
|
||||
giftBatchId: _rocketRewardRecordKey(record),
|
||||
);
|
||||
}
|
||||
|
||||
Msg _buildRocketRewardFallbackMsg(
|
||||
String groupId,
|
||||
Map<String, dynamic> payload,
|
||||
String userId,
|
||||
) {
|
||||
final rewardCover = _firstNonBlank([
|
||||
_payloadText(payload['rewardCover']),
|
||||
_payloadText(payload['rewardIcon']),
|
||||
_payloadText(payload['rewardUrl']),
|
||||
_payloadText(payload['giftUrl']),
|
||||
_payloadText(payload['giftIcon']),
|
||||
]);
|
||||
final rewardType = _firstNonBlank([
|
||||
_payloadText(payload['rewardType']),
|
||||
_payloadText(payload['type']),
|
||||
rewardCover.isEmpty ? 'GOLD' : '',
|
||||
]);
|
||||
final level = (_payloadNum(payload['level']) ?? 1).toInt();
|
||||
return Msg(
|
||||
groupId: groupId,
|
||||
msg: rewardCover,
|
||||
type: SCRoomMsgType.rocketRewardUser,
|
||||
user: _rocketRewardUserFromPayload(payload, userId),
|
||||
role: _rocketRewardRole(rewardType, rewardCover),
|
||||
number: level <= 0 ? 1 : level,
|
||||
giftBatchId: [
|
||||
'rocket_reward_fallback',
|
||||
_payloadText(payload['launchNo']),
|
||||
userId,
|
||||
level,
|
||||
rewardType,
|
||||
].join('|'),
|
||||
);
|
||||
}
|
||||
|
||||
SocialChatUserProfile _rocketRewardUserFromPayload(
|
||||
Map<String, dynamic> payload,
|
||||
String userId,
|
||||
) {
|
||||
final userPayload = _rocketRewardUserPayload(payload, userId);
|
||||
final nickname = _firstNonBlank([
|
||||
_payloadText(userPayload['userNickname']),
|
||||
_payloadText(userPayload['nickname']),
|
||||
_payloadText(userPayload['nickName']),
|
||||
_payloadText(userPayload['account']),
|
||||
userId,
|
||||
]);
|
||||
return SocialChatUserProfile(
|
||||
id: _firstNonBlank([_payloadText(userPayload['userId']), userId]),
|
||||
userNickname: nickname,
|
||||
userAvatar: _firstNonBlank([
|
||||
_payloadText(userPayload['userAvatar']),
|
||||
_payloadText(userPayload['avatar']),
|
||||
_payloadText(userPayload['avatarUrl']),
|
||||
]),
|
||||
account: _payloadText(userPayload['account']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _rocketRewardUserPayload(
|
||||
Map<String, dynamic> payload,
|
||||
String userId,
|
||||
) {
|
||||
final candidates = [
|
||||
payload['users'],
|
||||
payload['rewardUsers'],
|
||||
payload['winnerUsers'],
|
||||
payload['winners'],
|
||||
];
|
||||
for (final candidate in candidates) {
|
||||
if (candidate is Map) {
|
||||
final item = candidate[userId] ?? candidate[userId.toString()];
|
||||
final map = _broadcastPayloadMap(item);
|
||||
if (map.isNotEmpty) {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
if (candidate is Iterable) {
|
||||
for (final item in candidate) {
|
||||
final map = _broadcastPayloadMap(item);
|
||||
final id = _firstNonBlank([
|
||||
_payloadText(map['userId']),
|
||||
_payloadText(map['id']),
|
||||
_payloadText(map['account']),
|
||||
]);
|
||||
if (id == userId) {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return <String, dynamic>{'userId': userId, 'userNickname': userId};
|
||||
}
|
||||
|
||||
String _rocketRewardRole(String rewardType, String rewardCover) {
|
||||
final normalized = rewardType.trim().toUpperCase();
|
||||
if (normalized == 'GOLD' || normalized == 'COIN' || normalized == 'COINS') {
|
||||
return 'GOLD';
|
||||
}
|
||||
return rewardCover.trim().isEmpty ? 'GOLD' : normalized;
|
||||
}
|
||||
|
||||
String _rocketRewardRecordKey(SCRoomRocketRewardRecordRes record) {
|
||||
return [
|
||||
'rocket_reward',
|
||||
record.id,
|
||||
record.launchNo,
|
||||
record.userId,
|
||||
record.rewardScene,
|
||||
record.rewardType,
|
||||
record.rewardItemId,
|
||||
].join('|');
|
||||
}
|
||||
|
||||
Future<void> _showRoomRocketRewardPopupDialog(String roomId) async {
|
||||
try {
|
||||
final res = await SCChatRoomRepository().roomRocketRewardPopups(roomId);
|
||||
if (res.records.isEmpty) {
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
_sendRoomRocketRewardPublicMessages(roomId, {
|
||||
'records': res.records.map((item) => item.toJson()).toList(),
|
||||
}),
|
||||
);
|
||||
final recordIds = res.records
|
||||
.map((item) => item.id)
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
SmartDialog.show(
|
||||
tag: _roomRocketRewardDialogTag,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => RoomRocketRewardDialogLoader(
|
||||
roomId: roomId,
|
||||
initialPopupRecords: res.records,
|
||||
onClose: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
if (recordIds.isNotEmpty) {
|
||||
unawaited(
|
||||
SCChatRoomRepository().ackRoomRocketRewardPopups(recordIds),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
SCFloatingMessage _buildRoomRocketLaunchFloatingMessage(
|
||||
RoomRocketLaunchBroadcastMessage launch,
|
||||
) {
|
||||
return SCFloatingMessage(
|
||||
type: 3,
|
||||
roomId: launch.roomId,
|
||||
rocketLevel: launch.safeLevel,
|
||||
userAvatarUrl: _firstNonBlank([
|
||||
launch.roomCoverUrl,
|
||||
launch.triggerUser.avatar,
|
||||
]),
|
||||
userName: _firstNonBlank([launch.roomName, launch.roomAccount]),
|
||||
userId: _firstNonBlank([
|
||||
launch.triggerUser.account,
|
||||
launch.triggerUser.userId,
|
||||
launch.triggerUserId,
|
||||
]),
|
||||
giftUrl: launch.rocketIconUrl,
|
||||
fallbackUrl: launch.rocketIconUrl,
|
||||
giftId: launch.launchNo,
|
||||
durationSeconds: launch.durationSeconds,
|
||||
priority: 1000,
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _payloadStringList(dynamic value) {
|
||||
if (value is Iterable) {
|
||||
return value
|
||||
@ -1951,16 +2360,44 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
} else if (type == "ROCKET_ENERGY_LAUNCH") {
|
||||
///火箭触发飘屏
|
||||
var fdata = data["data"];
|
||||
final rocketIconUrl = _firstNonBlank([
|
||||
_payloadText(fdata["rocketIconUrl"]),
|
||||
_payloadText(fdata["rocketIcon"]),
|
||||
_payloadText(fdata["rocketUrl"]),
|
||||
_payloadText(fdata["rocketImageUrl"]),
|
||||
_payloadText(fdata["giftUrl"]),
|
||||
_payloadText(fdata["giftIcon"]),
|
||||
]);
|
||||
SCFloatingMessage msg = SCFloatingMessage(
|
||||
type: 3,
|
||||
roomId: fdata["roomId"],
|
||||
rocketLevel: fdata["fromLevel"],
|
||||
userAvatarUrl: fdata["userAvatar"],
|
||||
userName: fdata["nickname"],
|
||||
userAvatarUrl: _firstNonBlank([
|
||||
_payloadText(fdata["roomCoverUrl"]),
|
||||
_payloadText(fdata["roomCover"]),
|
||||
_payloadText(fdata["roomAvatar"]),
|
||||
_payloadText(fdata["roomIcon"]),
|
||||
_payloadText(fdata["coverUrl"]),
|
||||
_payloadText(fdata["cover"]),
|
||||
_payloadText(fdata["userAvatar"]),
|
||||
]),
|
||||
userName: _firstNonBlank([
|
||||
_payloadText(fdata["roomName"]),
|
||||
_payloadText(fdata["roomAccount"]),
|
||||
_payloadText(fdata["nickname"]),
|
||||
]),
|
||||
userId: fdata["actualAccount"],
|
||||
giftUrl: rocketIconUrl,
|
||||
fallbackUrl: rocketIconUrl,
|
||||
priority: 1000,
|
||||
);
|
||||
OverlayManager().addMessage(msg);
|
||||
} else if (type == SCRoomMsgType.voiceRoomRocketLaunchBroadcast) {
|
||||
_handleVoiceRoomRocketLaunchBroadcast(data["data"]);
|
||||
} else if (type == SCRoomMsgType.voiceRoomRocketStatusUpdate) {
|
||||
_handleVoiceRoomRocketStatusUpdate(data["data"]);
|
||||
} else if (type == SCRoomMsgType.voiceRoomRocketRewardPopup) {
|
||||
_handleVoiceRoomRocketRewardPopup(data["data"]);
|
||||
} else if (type == SCRoomMsgType.roomRedPacket) {
|
||||
///红包触发飘屏
|
||||
var fData = data["data"];
|
||||
@ -2049,6 +2486,18 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
// 直接处理字符串格式的自定义数据
|
||||
final data = json.decode(customData);
|
||||
|
||||
if (data["type"] == SCRoomMsgType.voiceRoomRocketLaunchBroadcast) {
|
||||
_handleVoiceRoomRocketLaunchBroadcast(data["data"]);
|
||||
return;
|
||||
}
|
||||
if (data["type"] == SCRoomMsgType.voiceRoomRocketStatusUpdate) {
|
||||
_handleVoiceRoomRocketStatusUpdate(data["data"]);
|
||||
return;
|
||||
}
|
||||
if (data["type"] == SCRoomMsgType.voiceRoomRocketRewardPopup) {
|
||||
_handleVoiceRoomRocketRewardPopup(data["data"]);
|
||||
return;
|
||||
}
|
||||
if (data["type"] == SCRoomMsgType.roomRedPacket) {
|
||||
///房间红包
|
||||
var fData = data["data"];
|
||||
|
||||
@ -10,7 +10,6 @@ import 'package:yumi/shared/tools/sc_version_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
@ -64,12 +63,22 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
|
||||
SCLoadingManager.show();
|
||||
if (uid.isNotEmpty) {
|
||||
await DataPersistence.setPendingChannelAuth(authType, uid);
|
||||
final res = await Future.wait([
|
||||
SCAccountRepository().loginForChannel(authType, uid),
|
||||
SCVersionUtils.checkReview(),
|
||||
]);
|
||||
var user = (res[0] as SocialChatLoginRes);
|
||||
var user = await SCAccountRepository().loginForChannel(authType, uid);
|
||||
if (!context.mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
AccountStorage().setCurrentUser(user);
|
||||
await DataPersistence.clearPendingChannelAuth();
|
||||
if (!context.mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
final canContinue = await SCVersionUtils.checkReview(
|
||||
context: context,
|
||||
);
|
||||
if (!context.mounted || !canContinue) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
await Provider.of<RtcProvider>(
|
||||
@ -84,11 +93,6 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
AccountStorage().setCurrentUser(user);
|
||||
await DataPersistence.clearPendingChannelAuth();
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
|
||||
@ -109,12 +113,22 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
|
||||
SCLoadingManager.show();
|
||||
if (uid.isNotEmpty) {
|
||||
await DataPersistence.setPendingChannelAuth(authType, uid);
|
||||
final res = await Future.wait([
|
||||
SCAccountRepository().loginForChannel(authType, uid),
|
||||
SCVersionUtils.checkReview(),
|
||||
]);
|
||||
var user = (res[0] as SocialChatLoginRes);
|
||||
var user = await SCAccountRepository().loginForChannel(authType, uid);
|
||||
if (!context.mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
AccountStorage().setCurrentUser(user);
|
||||
await DataPersistence.clearPendingChannelAuth();
|
||||
if (!context.mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
final canContinue = await SCVersionUtils.checkReview(
|
||||
context: context,
|
||||
);
|
||||
if (!context.mounted || !canContinue) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
await Provider.of<RtcProvider>(
|
||||
@ -129,11 +143,6 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
AccountStorage().setCurrentUser(user);
|
||||
await DataPersistence.clearPendingChannelAuth();
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
@ -95,8 +96,26 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
SCUserIdentityRes? userIdentity;
|
||||
|
||||
void getUserIdentity() async {
|
||||
userIdentity = await SCAccountRepository().userIdentity();
|
||||
notifyListeners();
|
||||
if (kDebugMode) {
|
||||
final profile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
debugPrint(
|
||||
'[SCUserIdentity] request start userId=${profile?.id}, account=${profile?.account}',
|
||||
);
|
||||
}
|
||||
try {
|
||||
userIdentity = await SCAccountRepository().userIdentity();
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[SCUserIdentity] manager stored admin=${userIdentity?.admin}, '
|
||||
'manager=${userIdentity?.manager}, yumiManager=${userIdentity?.yumiManager}',
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (error) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[SCUserIdentity] request failed error=$error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前正在查看的个人详情资料,不用于承载登录用户自己的 Me 页数据。
|
||||
|
||||
@ -9,6 +9,7 @@ import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/models/country_mode.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_banner_leaderboard_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/country_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_backpack_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart';
|
||||
@ -17,6 +18,7 @@ import '../../shared/data_sources/models/enum/sc_banner_type.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_gift_type.dart';
|
||||
|
||||
class SCAppGeneralManager extends ChangeNotifier {
|
||||
static const String backpackGiftTab = 'backpack';
|
||||
static const int _maxInitialFullScreenGiftPreloads = 4;
|
||||
|
||||
List<CountryMode> countryModeList = [];
|
||||
@ -32,9 +34,12 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
final Set<String> _warmingGiftCoverUrls = <String>{};
|
||||
bool _isFetchingGiftList = false;
|
||||
bool _hasGiftListLoaded = false;
|
||||
bool _isFetchingGiftBackpack = false;
|
||||
bool _hasGiftBackpackLoaded = false;
|
||||
|
||||
Map<String, List<SocialChatGiftRes>> giftByTab = {};
|
||||
Map<String, List<SocialChatGiftRes>> activityGiftByTab = {};
|
||||
List<SocialChatGiftBackpackRes> giftBackpackResList = [];
|
||||
|
||||
///选中的国家
|
||||
Country? selectCountryInfo;
|
||||
@ -51,6 +56,13 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
|
||||
bool get isGiftListLoading => _isFetchingGiftList && !_hasGiftListLoaded;
|
||||
|
||||
bool get isGiftBackpackLoading =>
|
||||
_isFetchingGiftBackpack && !_hasGiftBackpackLoaded;
|
||||
|
||||
bool isGiftTabLoading(String type) {
|
||||
return type == backpackGiftTab ? isGiftBackpackLoading : isGiftListLoading;
|
||||
}
|
||||
|
||||
///加载国家列表
|
||||
Future<void> fetchCountryList() async {
|
||||
if (_isFetchingCountryList) {
|
||||
@ -203,6 +215,7 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
} else {
|
||||
giftByTab.remove("CUSTOMIZED");
|
||||
}
|
||||
_mergeBackpackGiftTab();
|
||||
}
|
||||
|
||||
Future<void> warmGiftPageCovers(
|
||||
@ -270,12 +283,75 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
///礼物背包
|
||||
void giftBackpack() async {
|
||||
try {
|
||||
await SCChatRoomRepository().giftBackpack();
|
||||
} catch (_) {
|
||||
Future<void> giftBackpack({bool forceRefresh = false}) async {
|
||||
if (_isFetchingGiftBackpack) {
|
||||
return;
|
||||
}
|
||||
if (_hasGiftBackpackLoaded && !forceRefresh) {
|
||||
_mergeBackpackGiftTab();
|
||||
notifyListeners();
|
||||
unawaited(
|
||||
warmGiftPageCovers(
|
||||
backpackGiftTab,
|
||||
pageIndex: 0,
|
||||
pageCount: 2,
|
||||
pageSize: 8,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_isFetchingGiftBackpack = true;
|
||||
if (!_hasGiftBackpackLoaded) {
|
||||
notifyListeners();
|
||||
}
|
||||
try {
|
||||
giftBackpackResList = await SCChatRoomRepository().giftBackpack();
|
||||
_hasGiftBackpackLoaded = true;
|
||||
_mergeBackpackGiftTab();
|
||||
notifyListeners();
|
||||
unawaited(
|
||||
warmGiftPageCovers(
|
||||
backpackGiftTab,
|
||||
pageIndex: 0,
|
||||
pageCount: 2,
|
||||
pageSize: 8,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
return;
|
||||
} finally {
|
||||
_isFetchingGiftBackpack = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _mergeBackpackGiftTab() {
|
||||
final backpackGifts = <SocialChatGiftRes>[];
|
||||
for (final item in giftBackpackResList) {
|
||||
final giftConfig = item.giftConfig;
|
||||
if (giftConfig == null) {
|
||||
continue;
|
||||
}
|
||||
backpackGifts.add(
|
||||
giftConfig.copyWith(
|
||||
giftTab: backpackGiftTab,
|
||||
quantity: _formatBackpackQuantity(item.quantity),
|
||||
backpackId: item.id,
|
||||
backpackQuantity: item.quantity,
|
||||
),
|
||||
);
|
||||
}
|
||||
giftByTab[backpackGiftTab] = backpackGifts;
|
||||
}
|
||||
|
||||
String _formatBackpackQuantity(num? quantity) {
|
||||
if (quantity == null) {
|
||||
return '0';
|
||||
}
|
||||
if (quantity % 1 == 0) {
|
||||
return quantity.toInt().toString();
|
||||
}
|
||||
return quantity.toString();
|
||||
}
|
||||
|
||||
///礼物id获取礼物
|
||||
|
||||
@ -34,6 +34,8 @@ class SocialChatGiftRes {
|
||||
String? userId,
|
||||
String? giftName,
|
||||
String? quantity,
|
||||
String? backpackId,
|
||||
num? backpackQuantity,
|
||||
num? expiredTime,
|
||||
}) {
|
||||
_id = id;
|
||||
@ -54,6 +56,8 @@ class SocialChatGiftRes {
|
||||
_userId = userId;
|
||||
_giftName = giftName;
|
||||
_quantity = quantity;
|
||||
_backpackId = backpackId;
|
||||
_backpackQuantity = backpackQuantity;
|
||||
_expiredTime = expiredTime;
|
||||
}
|
||||
|
||||
@ -123,6 +127,12 @@ class SocialChatGiftRes {
|
||||
_quantity = _giftResString(
|
||||
json['quantity'] ?? json['giftQuantity'] ?? json['count'] ?? json['num'],
|
||||
);
|
||||
_backpackId = _giftResString(
|
||||
json['backpackId'] ?? json['backId'] ?? json['backpackRecordId'],
|
||||
);
|
||||
_backpackQuantity = _giftResNum(
|
||||
json['backpackQuantity'] ?? json['backpackCount'],
|
||||
);
|
||||
_expiredTime = _giftResNum(
|
||||
json['expiredTime'] ?? giftConfig?['expiredTime'],
|
||||
);
|
||||
@ -145,6 +155,8 @@ class SocialChatGiftRes {
|
||||
String? _userId;
|
||||
String? _giftName;
|
||||
String? _quantity;
|
||||
String? _backpackId;
|
||||
num? _backpackQuantity;
|
||||
num? _expiredTime;
|
||||
SocialChatGiftRes copyWith({
|
||||
String? id,
|
||||
@ -165,6 +177,8 @@ class SocialChatGiftRes {
|
||||
String? userId,
|
||||
String? giftName,
|
||||
String? quantity,
|
||||
String? backpackId,
|
||||
num? backpackQuantity,
|
||||
num? expiredTime,
|
||||
}) => SocialChatGiftRes(
|
||||
id: id ?? _id,
|
||||
@ -185,6 +199,8 @@ class SocialChatGiftRes {
|
||||
userId: userId ?? _userId,
|
||||
giftName: giftName ?? _giftName,
|
||||
quantity: quantity ?? _quantity,
|
||||
backpackId: backpackId ?? _backpackId,
|
||||
backpackQuantity: backpackQuantity ?? _backpackQuantity,
|
||||
expiredTime: expiredTime ?? _expiredTime,
|
||||
);
|
||||
String? get id => _id;
|
||||
@ -205,6 +221,8 @@ class SocialChatGiftRes {
|
||||
String? get userId => _userId;
|
||||
String? get giftName => _giftName;
|
||||
String? get quantity => _quantity;
|
||||
String? get backpackId => _backpackId;
|
||||
num? get backpackQuantity => _backpackQuantity;
|
||||
num? get expiredTime => _expiredTime;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@ -227,6 +245,8 @@ class SocialChatGiftRes {
|
||||
map['userId'] = _userId;
|
||||
map['giftName'] = _giftName;
|
||||
map['quantity'] = _quantity;
|
||||
map['backpackId'] = _backpackId;
|
||||
map['backpackQuantity'] = _backpackQuantity;
|
||||
map['expiredTime'] = _expiredTime;
|
||||
return map;
|
||||
}
|
||||
|
||||
@ -878,20 +878,30 @@ class WearBadge {
|
||||
_userId = userId;
|
||||
}
|
||||
|
||||
WearBadge.fromJson(dynamic json) {
|
||||
_animationUrl = json['animationUrl'];
|
||||
_badgeKey = json['badgeKey'];
|
||||
_badgeLevel = json['badgeLevel'];
|
||||
_badgeName = json['badgeName'];
|
||||
_expireTime = json['expireTime'];
|
||||
_id = json['id'];
|
||||
_use = json['use'];
|
||||
_milestone = json['milestone'];
|
||||
_notSelectUrl = json['notSelectUrl'];
|
||||
_selectUrl = json['selectUrl'];
|
||||
_type = json['type'];
|
||||
_userId = json['userId'];
|
||||
}
|
||||
WearBadge.fromJson(dynamic json) {
|
||||
if (json is! Map) {
|
||||
return;
|
||||
}
|
||||
_animationUrl = _stringValue(json['animationUrl']);
|
||||
_badgeKey = _stringValue(json['badgeKey']);
|
||||
_badgeLevel = _numValue(json['badgeLevel'] ?? json['level']);
|
||||
_badgeName = _stringValue(
|
||||
json['badgeName'] ?? json['name'] ?? json['displayName'],
|
||||
);
|
||||
_expireTime = _intValue(json['expireTime'] ?? json['expireAt']);
|
||||
_id = _stringValue(json['id'] ?? json['badgeId']);
|
||||
_use = _boolValue(json['use']);
|
||||
_milestone = _stringValue(json['milestone']);
|
||||
_notSelectUrl = _stringValue(json['notSelectUrl']);
|
||||
_selectUrl = _stringValue(
|
||||
json['url'] ??
|
||||
json['selectUrl'] ??
|
||||
json['notSelectUrl'] ??
|
||||
json['animationUrl'],
|
||||
);
|
||||
_type = _stringValue(json['type']);
|
||||
_userId = _stringValue(json['userId']);
|
||||
}
|
||||
|
||||
String? _animationUrl;
|
||||
String? _badgeKey;
|
||||
@ -958,7 +968,7 @@ class WearBadge {
|
||||
|
||||
String? get userId => _userId;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['animationUrl'] = _animationUrl;
|
||||
map['badgeKey'] = _badgeKey;
|
||||
@ -972,9 +982,48 @@ class WearBadge {
|
||||
map['selectUrl'] = _selectUrl;
|
||||
map['type'] = _type;
|
||||
map['userId'] = _userId;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
static String? _stringValue(dynamic value) {
|
||||
final text = value?.toString().trim();
|
||||
return text == null || text.isEmpty ? null : text;
|
||||
}
|
||||
|
||||
static int? _intValue(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(value?.toString() ?? "");
|
||||
}
|
||||
|
||||
static num? _numValue(dynamic value) {
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
return num.tryParse(value?.toString() ?? "");
|
||||
}
|
||||
|
||||
static bool? _boolValue(dynamic value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
final text = value?.toString().trim().toLowerCase();
|
||||
if (text == "true" || text == "1") {
|
||||
return true;
|
||||
}
|
||||
if (text == "false" || text == "0") {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class PersonPhoto {
|
||||
PersonPhoto({String? url, num? status}) {
|
||||
|
||||
741
lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart
Normal file
@ -0,0 +1,741 @@
|
||||
class SCRoomRocketLevelRes {
|
||||
SCRoomRocketLevelRes({
|
||||
required this.id,
|
||||
required this.level,
|
||||
required this.needEnergy,
|
||||
required this.rocketIconUrl,
|
||||
required this.rocketPreviewUrl,
|
||||
required this.rocketAnimationUrl,
|
||||
required this.progressBarUrl,
|
||||
required this.shakeThresholdPercent,
|
||||
required this.enabled,
|
||||
required this.sort,
|
||||
});
|
||||
|
||||
factory SCRoomRocketLevelRes.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
final rocket = _asMap(data['rocket']);
|
||||
final levelInfo = _asMap(data['levelInfo']);
|
||||
final config = _asMap(data['config']);
|
||||
final resource = _asMap(
|
||||
data['resource'] ?? data['propsResources'] ?? data['rocketResource'],
|
||||
);
|
||||
final preview = _asMap(
|
||||
data['rocketPreview'] ??
|
||||
data['preview'] ??
|
||||
data['rocketPreviewResource'] ??
|
||||
data['previewResource'],
|
||||
);
|
||||
final animation = _asMap(
|
||||
data['rocketAnimation'] ??
|
||||
data['animation'] ??
|
||||
data['rocketAnimationResource'] ??
|
||||
data['animationResource'] ??
|
||||
data['launchAnimation'],
|
||||
);
|
||||
return SCRoomRocketLevelRes(
|
||||
id: _stringValue(data['id']),
|
||||
level: _intValue(data['level']),
|
||||
needEnergy: _intValue(data['needEnergy']),
|
||||
rocketIconUrl: _firstAssetString([
|
||||
data['rocket_icon_url'],
|
||||
data['rocketIconUrl'],
|
||||
data['iconUrl'],
|
||||
data['icon'],
|
||||
data['rocketIcon'],
|
||||
rocket['rocket_icon_url'],
|
||||
rocket['rocketIconUrl'],
|
||||
rocket['iconUrl'],
|
||||
rocket['icon'],
|
||||
rocket['rocketIcon'],
|
||||
levelInfo['rocket_icon_url'],
|
||||
levelInfo['rocketIconUrl'],
|
||||
levelInfo['iconUrl'],
|
||||
levelInfo['rocketIcon'],
|
||||
config['rocket_icon_url'],
|
||||
config['rocketIconUrl'],
|
||||
config['iconUrl'],
|
||||
config['rocketIcon'],
|
||||
resource['rocket_icon_url'],
|
||||
resource['rocketIconUrl'],
|
||||
resource['iconUrl'],
|
||||
resource['cover'],
|
||||
resource['sourceUrl'],
|
||||
]),
|
||||
rocketPreviewUrl: _firstAssetString([
|
||||
data['preview_rocket_url'],
|
||||
data['previewRocketUrl'],
|
||||
data['rocketPreviewUrl'],
|
||||
data['rocketPreviewSvgUrl'],
|
||||
data['rocketPreviewSvg'],
|
||||
data['rocketPreview'],
|
||||
data['rocketSvgUrl'],
|
||||
data['rocketSvg'],
|
||||
data['previewSvgUrl'],
|
||||
data['previewSvg'],
|
||||
data['previewUrl'],
|
||||
data['preview'],
|
||||
data['svgUrl'],
|
||||
data['svg'],
|
||||
data['rocketDisplayUrl'],
|
||||
data['rocketShowUrl'],
|
||||
data['rocketImageUrl'],
|
||||
data['previewImageUrl'],
|
||||
data['sourceUrl'],
|
||||
rocket['preview_rocket_url'],
|
||||
rocket['previewRocketUrl'],
|
||||
rocket['rocketPreviewUrl'],
|
||||
rocket['rocketPreviewSvgUrl'],
|
||||
rocket['rocketPreviewSvg'],
|
||||
rocket['rocketPreview'],
|
||||
rocket['rocketSvgUrl'],
|
||||
rocket['rocketSvg'],
|
||||
rocket['previewSvgUrl'],
|
||||
rocket['previewSvg'],
|
||||
rocket['previewUrl'],
|
||||
rocket['preview'],
|
||||
rocket['svgUrl'],
|
||||
rocket['sourceUrl'],
|
||||
levelInfo['preview_rocket_url'],
|
||||
levelInfo['previewRocketUrl'],
|
||||
levelInfo['rocketPreviewUrl'],
|
||||
levelInfo['rocketPreviewSvgUrl'],
|
||||
levelInfo['rocketPreviewSvg'],
|
||||
levelInfo['rocketPreview'],
|
||||
levelInfo['previewUrl'],
|
||||
levelInfo['previewSvgUrl'],
|
||||
levelInfo['previewSvg'],
|
||||
levelInfo['preview'],
|
||||
levelInfo['svgUrl'],
|
||||
levelInfo['sourceUrl'],
|
||||
config['preview_rocket_url'],
|
||||
config['previewRocketUrl'],
|
||||
config['rocketPreviewUrl'],
|
||||
config['rocketPreviewSvgUrl'],
|
||||
config['rocketPreviewSvg'],
|
||||
config['rocketPreview'],
|
||||
config['previewUrl'],
|
||||
config['previewSvgUrl'],
|
||||
config['previewSvg'],
|
||||
config['preview'],
|
||||
config['svgUrl'],
|
||||
config['sourceUrl'],
|
||||
preview,
|
||||
resource['preview_rocket_url'],
|
||||
resource['previewRocketUrl'],
|
||||
resource['rocketPreviewUrl'],
|
||||
resource['previewUrl'],
|
||||
resource['sourceUrl'],
|
||||
]),
|
||||
rocketAnimationUrl: _firstAssetString([
|
||||
data['rocket_animation_url'],
|
||||
data['rocketAnimationUrl'],
|
||||
data['rocketSvgaUrl'],
|
||||
data['animationUrl'],
|
||||
data['rocketAnimation'],
|
||||
data['animation'],
|
||||
data['svgaUrl'],
|
||||
data['rocketLaunchSvgaUrl'],
|
||||
data['launchAnimationUrl'],
|
||||
data['launchAnimation'],
|
||||
data['launchSvgaUrl'],
|
||||
data['sourceUrl'],
|
||||
rocket['rocket_animation_url'],
|
||||
rocket['rocketAnimationUrl'],
|
||||
rocket['rocketSvgaUrl'],
|
||||
rocket['animationUrl'],
|
||||
rocket['rocketAnimation'],
|
||||
rocket['animation'],
|
||||
rocket['svgaUrl'],
|
||||
rocket['sourceUrl'],
|
||||
levelInfo['rocket_animation_url'],
|
||||
levelInfo['rocketAnimationUrl'],
|
||||
levelInfo['rocketSvgaUrl'],
|
||||
levelInfo['animationUrl'],
|
||||
levelInfo['rocketAnimation'],
|
||||
levelInfo['animation'],
|
||||
levelInfo['svgaUrl'],
|
||||
levelInfo['sourceUrl'],
|
||||
config['rocket_animation_url'],
|
||||
config['rocketAnimationUrl'],
|
||||
config['rocketSvgaUrl'],
|
||||
config['animationUrl'],
|
||||
config['rocketAnimation'],
|
||||
config['animation'],
|
||||
config['svgaUrl'],
|
||||
config['sourceUrl'],
|
||||
animation,
|
||||
]),
|
||||
progressBarUrl: _firstAssetString([
|
||||
data['progress_bar_url'],
|
||||
data['progressBarUrl'],
|
||||
data['progressUrl'],
|
||||
data['progressBar'],
|
||||
rocket['progress_bar_url'],
|
||||
rocket['progressBarUrl'],
|
||||
levelInfo['progress_bar_url'],
|
||||
levelInfo['progressBarUrl'],
|
||||
config['progress_bar_url'],
|
||||
config['progressBarUrl'],
|
||||
]),
|
||||
shakeThresholdPercent:
|
||||
_numValue(
|
||||
data['shake_threshold_percent'] ??
|
||||
data['shakeThresholdPercent'] ??
|
||||
rocket['shake_threshold_percent'] ??
|
||||
rocket['shakeThresholdPercent'] ??
|
||||
levelInfo['shake_threshold_percent'] ??
|
||||
levelInfo['shakeThresholdPercent'] ??
|
||||
config['shake_threshold_percent'] ??
|
||||
config['shakeThresholdPercent'],
|
||||
) ??
|
||||
95,
|
||||
enabled: _boolValue(data['enabled'], fallback: true),
|
||||
sort: _intValue(data['sort']),
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final int level;
|
||||
final int needEnergy;
|
||||
final String rocketIconUrl;
|
||||
final String rocketPreviewUrl;
|
||||
final String rocketAnimationUrl;
|
||||
final String progressBarUrl;
|
||||
final num shakeThresholdPercent;
|
||||
final bool enabled;
|
||||
final int sort;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'level': level,
|
||||
'needEnergy': needEnergy,
|
||||
'rocketIconUrl': rocketIconUrl,
|
||||
'rocketPreviewUrl': rocketPreviewUrl,
|
||||
'rocketAnimationUrl': rocketAnimationUrl,
|
||||
'progressBarUrl': progressBarUrl,
|
||||
'shakeThresholdPercent': shakeThresholdPercent,
|
||||
'enabled': enabled,
|
||||
'sort': sort,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomRocketRewardRes {
|
||||
SCRoomRocketRewardRes({
|
||||
required this.rewardScene,
|
||||
required this.rewardType,
|
||||
required this.rewardItemId,
|
||||
required this.rewardName,
|
||||
required this.rewardCover,
|
||||
required this.rewardAmount,
|
||||
required this.expireDays,
|
||||
});
|
||||
|
||||
factory SCRoomRocketRewardRes.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
return SCRoomRocketRewardRes(
|
||||
rewardScene: _stringValue(data['rewardScene']),
|
||||
rewardType: _stringValue(data['rewardType']),
|
||||
rewardItemId: _stringValue(data['rewardItemId']),
|
||||
rewardName: _stringValue(data['rewardName']),
|
||||
rewardCover: _stringValue(data['rewardCover']),
|
||||
rewardAmount: _numValue(data['rewardAmount']) ?? 0,
|
||||
expireDays: _intValue(data['expireDays']),
|
||||
);
|
||||
}
|
||||
|
||||
final String rewardScene;
|
||||
final String rewardType;
|
||||
final String rewardItemId;
|
||||
final String rewardName;
|
||||
final String rewardCover;
|
||||
final num rewardAmount;
|
||||
final int expireDays;
|
||||
|
||||
String get displayAmount {
|
||||
if (rewardAmount <= 0) {
|
||||
return '1';
|
||||
}
|
||||
if (rewardAmount % 1 == 0) {
|
||||
return rewardAmount.toInt().toString();
|
||||
}
|
||||
return rewardAmount.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomRocketRewardPreviewRes {
|
||||
SCRoomRocketRewardPreviewRes({
|
||||
required this.top1,
|
||||
required this.ignite,
|
||||
required this.inRoom,
|
||||
});
|
||||
|
||||
factory SCRoomRocketRewardPreviewRes.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
return SCRoomRocketRewardPreviewRes(
|
||||
top1: _rewardList(data['top1']),
|
||||
ignite: _rewardList(data['ignite']),
|
||||
inRoom: _rewardList(data['inRoom']),
|
||||
);
|
||||
}
|
||||
|
||||
final List<SCRoomRocketRewardRes> top1;
|
||||
final List<SCRoomRocketRewardRes> ignite;
|
||||
final List<SCRoomRocketRewardRes> inRoom;
|
||||
|
||||
List<SCRoomRocketRewardRes> get all => [...top1, ...ignite, ...inRoom];
|
||||
|
||||
static List<SCRoomRocketRewardRes> _rewardList(dynamic value) {
|
||||
if (value is! Iterable) {
|
||||
return const <SCRoomRocketRewardRes>[];
|
||||
}
|
||||
return value.map(SCRoomRocketRewardRes.fromJson).toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomRocketKingUserRes {
|
||||
SCRoomRocketKingUserRes({
|
||||
required this.rank,
|
||||
required this.userId,
|
||||
required this.userAvatar,
|
||||
required this.userNickname,
|
||||
required this.account,
|
||||
required this.countryCode,
|
||||
required this.countryName,
|
||||
required this.scoreEnergy,
|
||||
});
|
||||
|
||||
factory SCRoomRocketKingUserRes.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
final profile = _asMap(data['userProfile'] ?? data['user']);
|
||||
return SCRoomRocketKingUserRes(
|
||||
rank: _intValue(data['rank'] ?? data['ranking']),
|
||||
userId: _stringValue(data['userId'] ?? profile['id']),
|
||||
userAvatar: _stringValue(
|
||||
data['userAvatar'] ??
|
||||
data['avatar'] ??
|
||||
data['avatarUrl'] ??
|
||||
profile['userAvatar'] ??
|
||||
profile['avatar'],
|
||||
),
|
||||
userNickname: _stringValue(
|
||||
data['userNickname'] ??
|
||||
data['nickname'] ??
|
||||
data['nickName'] ??
|
||||
profile['userNickname'] ??
|
||||
profile['nickname'] ??
|
||||
profile['nickName'],
|
||||
),
|
||||
account: _stringValue(data['account'] ?? profile['account']),
|
||||
countryCode: _stringValue(data['countryCode'] ?? profile['countryCode']),
|
||||
countryName: _stringValue(data['countryName'] ?? profile['countryName']),
|
||||
scoreEnergy: _numValue(data['scoreEnergy'] ?? data['score']) ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
final int rank;
|
||||
final String userId;
|
||||
final String userAvatar;
|
||||
final String userNickname;
|
||||
final String account;
|
||||
final String countryCode;
|
||||
final String countryName;
|
||||
final num scoreEnergy;
|
||||
|
||||
String get displayName {
|
||||
if (userNickname.trim().isNotEmpty) {
|
||||
return userNickname;
|
||||
}
|
||||
if (account.trim().isNotEmpty) {
|
||||
return account;
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomRocketKingRes {
|
||||
SCRoomRocketKingRes({
|
||||
required this.roomId,
|
||||
required this.dayKey,
|
||||
required this.roundNo,
|
||||
required this.sourceRoundNo,
|
||||
required this.level,
|
||||
required this.podium,
|
||||
required this.records,
|
||||
required this.currentUser,
|
||||
required this.emptyReason,
|
||||
});
|
||||
|
||||
factory SCRoomRocketKingRes.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
return SCRoomRocketKingRes(
|
||||
roomId: _stringValue(data['roomId']),
|
||||
dayKey: _stringValue(data['dayKey']),
|
||||
roundNo: _intValue(data['roundNo']),
|
||||
sourceRoundNo: _intValue(data['sourceRoundNo']),
|
||||
level: _intValue(data['level']),
|
||||
podium: _kingList(data['podium']),
|
||||
records: _kingList(data['records']),
|
||||
currentUser: _kingUser(
|
||||
data['currentUser'] ?? data['mine'] ?? data['self'] ?? data['myRank'],
|
||||
),
|
||||
emptyReason: _stringValue(data['emptyReason']),
|
||||
);
|
||||
}
|
||||
|
||||
final String roomId;
|
||||
final String dayKey;
|
||||
final int roundNo;
|
||||
final int sourceRoundNo;
|
||||
final int level;
|
||||
final List<SCRoomRocketKingUserRes> podium;
|
||||
final List<SCRoomRocketKingUserRes> records;
|
||||
final SCRoomRocketKingUserRes? currentUser;
|
||||
final String emptyReason;
|
||||
|
||||
List<SCRoomRocketKingUserRes> get displayRecords {
|
||||
if (records.isNotEmpty) {
|
||||
return records;
|
||||
}
|
||||
return podium;
|
||||
}
|
||||
|
||||
static List<SCRoomRocketKingUserRes> _kingList(dynamic value) {
|
||||
if (value is! Iterable) {
|
||||
return const <SCRoomRocketKingUserRes>[];
|
||||
}
|
||||
return value.map(SCRoomRocketKingUserRes.fromJson).toList(growable: false);
|
||||
}
|
||||
|
||||
static SCRoomRocketKingUserRes? _kingUser(dynamic value) {
|
||||
if (value is! Map) {
|
||||
return null;
|
||||
}
|
||||
return SCRoomRocketKingUserRes.fromJson(value);
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomRocketRewardPopupRes {
|
||||
SCRoomRocketRewardPopupRes({
|
||||
required this.records,
|
||||
required this.cursor,
|
||||
required this.limit,
|
||||
required this.emptyReason,
|
||||
});
|
||||
|
||||
factory SCRoomRocketRewardPopupRes.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
return SCRoomRocketRewardPopupRes(
|
||||
records: SCRoomRocketRewardRecordRes.listFromJson(data['records']),
|
||||
cursor: _intValue(data['cursor'], fallback: 1),
|
||||
limit: _intValue(data['limit'], fallback: 10),
|
||||
emptyReason: _stringValue(data['emptyReason']),
|
||||
);
|
||||
}
|
||||
|
||||
final List<SCRoomRocketRewardRecordRes> records;
|
||||
final int cursor;
|
||||
final int limit;
|
||||
final String emptyReason;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"records": records.map((item) => item.toJson()).toList(growable: false),
|
||||
"cursor": cursor,
|
||||
"limit": limit,
|
||||
"emptyReason": emptyReason,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomRocketRewardRecordsRes {
|
||||
SCRoomRocketRewardRecordsRes({
|
||||
required this.records,
|
||||
required this.cursor,
|
||||
required this.limit,
|
||||
required this.emptyReason,
|
||||
});
|
||||
|
||||
factory SCRoomRocketRewardRecordsRes.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
return SCRoomRocketRewardRecordsRes(
|
||||
records: SCRoomRocketRewardRecordRes.listFromJson(data['records']),
|
||||
cursor: _intValue(data['cursor'], fallback: 1),
|
||||
limit: _intValue(data['limit'], fallback: 10),
|
||||
emptyReason: _stringValue(data['emptyReason']),
|
||||
);
|
||||
}
|
||||
|
||||
final List<SCRoomRocketRewardRecordRes> records;
|
||||
final int cursor;
|
||||
final int limit;
|
||||
final String emptyReason;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"records": records.map((item) => item.toJson()).toList(growable: false),
|
||||
"cursor": cursor,
|
||||
"limit": limit,
|
||||
"emptyReason": emptyReason,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SCRoomRocketRewardRecordRes {
|
||||
SCRoomRocketRewardRecordRes({
|
||||
required this.id,
|
||||
required this.launchNo,
|
||||
required this.roomId,
|
||||
required this.dayKey,
|
||||
required this.roundNo,
|
||||
required this.level,
|
||||
required this.userId,
|
||||
required this.userAvatar,
|
||||
required this.userNickname,
|
||||
required this.account,
|
||||
required this.rewardScene,
|
||||
required this.rewardType,
|
||||
required this.rewardItemId,
|
||||
required this.rewardName,
|
||||
required this.rewardCover,
|
||||
required this.rewardAmount,
|
||||
required this.expireDays,
|
||||
required this.grantStatus,
|
||||
required this.popupStatus,
|
||||
required this.grantTime,
|
||||
required this.createTime,
|
||||
required this.errorMessage,
|
||||
});
|
||||
|
||||
factory SCRoomRocketRewardRecordRes.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
return SCRoomRocketRewardRecordRes(
|
||||
id: _stringValue(data['id']),
|
||||
launchNo: _stringValue(data['launchNo']),
|
||||
roomId: _stringValue(data['roomId']),
|
||||
dayKey: _stringValue(data['dayKey']),
|
||||
roundNo: _intValue(data['roundNo']),
|
||||
level: _intValue(data['level']),
|
||||
userId: _stringValue(data['userId']),
|
||||
userAvatar: _stringValue(
|
||||
data['userAvatar'] ??
|
||||
data['avatar'] ??
|
||||
data['avatarUrl'] ??
|
||||
_asMap(data['userProfile'] ?? data['user'])['userAvatar'] ??
|
||||
_asMap(data['userProfile'] ?? data['user'])['avatar'],
|
||||
),
|
||||
userNickname: _stringValue(
|
||||
data['userNickname'] ??
|
||||
data['nickname'] ??
|
||||
data['nickName'] ??
|
||||
_asMap(data['userProfile'] ?? data['user'])['userNickname'] ??
|
||||
_asMap(data['userProfile'] ?? data['user'])['nickname'] ??
|
||||
_asMap(data['userProfile'] ?? data['user'])['nickName'],
|
||||
),
|
||||
account: _stringValue(
|
||||
data['account'] ??
|
||||
_asMap(data['userProfile'] ?? data['user'])['account'],
|
||||
),
|
||||
rewardScene: _stringValue(data['rewardScene']),
|
||||
rewardType: _stringValue(data['rewardType']),
|
||||
rewardItemId: _stringValue(data['rewardItemId']),
|
||||
rewardName: _stringValue(data['rewardName']),
|
||||
rewardCover: _stringValue(data['rewardCover']),
|
||||
rewardAmount: _numValue(data['rewardAmount']) ?? 0,
|
||||
expireDays: _intValue(data['expireDays']),
|
||||
grantStatus: _stringValue(data['grantStatus']),
|
||||
popupStatus: _stringValue(data['popupStatus']),
|
||||
grantTime: _stringValue(data['grantTime']),
|
||||
createTime: _stringValue(data['createTime']),
|
||||
errorMessage: _stringValue(data['errorMessage']),
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String launchNo;
|
||||
final String roomId;
|
||||
final String dayKey;
|
||||
final int roundNo;
|
||||
final int level;
|
||||
final String userId;
|
||||
final String userAvatar;
|
||||
final String userNickname;
|
||||
final String account;
|
||||
final String rewardScene;
|
||||
final String rewardType;
|
||||
final String rewardItemId;
|
||||
final String rewardName;
|
||||
final String rewardCover;
|
||||
final num rewardAmount;
|
||||
final int expireDays;
|
||||
final String grantStatus;
|
||||
final String popupStatus;
|
||||
final String grantTime;
|
||||
final String createTime;
|
||||
final String errorMessage;
|
||||
|
||||
String get displayName {
|
||||
if (userNickname.trim().isNotEmpty) {
|
||||
return userNickname;
|
||||
}
|
||||
if (account.trim().isNotEmpty) {
|
||||
return account;
|
||||
}
|
||||
if (userId.trim().isNotEmpty) {
|
||||
return 'ID:$userId';
|
||||
}
|
||||
return 'You';
|
||||
}
|
||||
|
||||
String get displayAmount {
|
||||
if (rewardAmount <= 0) {
|
||||
return '1';
|
||||
}
|
||||
if (rewardAmount % 1 == 0) {
|
||||
return rewardAmount.toInt().toString();
|
||||
}
|
||||
return rewardAmount.toString();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"id": id,
|
||||
"launchNo": launchNo,
|
||||
"roomId": roomId,
|
||||
"dayKey": dayKey,
|
||||
"roundNo": roundNo,
|
||||
"level": level,
|
||||
"userId": userId,
|
||||
"userAvatar": userAvatar,
|
||||
"userNickname": userNickname,
|
||||
"account": account,
|
||||
"rewardScene": rewardScene,
|
||||
"rewardType": rewardType,
|
||||
"rewardItemId": rewardItemId,
|
||||
"rewardName": rewardName,
|
||||
"rewardCover": rewardCover,
|
||||
"rewardAmount": rewardAmount,
|
||||
"expireDays": expireDays,
|
||||
"grantStatus": grantStatus,
|
||||
"popupStatus": popupStatus,
|
||||
"grantTime": grantTime,
|
||||
"createTime": createTime,
|
||||
"errorMessage": errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
static List<SCRoomRocketRewardRecordRes> listFromJson(dynamic value) {
|
||||
if (value is! Iterable) {
|
||||
return const <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
return value
|
||||
.map(SCRoomRocketRewardRecordRes.fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _asMap(dynamic value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
return value;
|
||||
}
|
||||
if (value is Map) {
|
||||
return value.map((key, item) => MapEntry(key.toString(), item));
|
||||
}
|
||||
return const <String, dynamic>{};
|
||||
}
|
||||
|
||||
String _stringValue(dynamic value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return value.toString().trim();
|
||||
}
|
||||
|
||||
String _firstAssetString(Iterable<dynamic> values) {
|
||||
for (final value in values) {
|
||||
final text = _assetString(value);
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String _assetString(dynamic value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
if (value is Map) {
|
||||
final map = _asMap(value);
|
||||
return _firstAssetString([
|
||||
map['sourceUrl'],
|
||||
map['url'],
|
||||
map['resourceUrl'],
|
||||
map['fileUrl'],
|
||||
map['src'],
|
||||
map['path'],
|
||||
map['cover'],
|
||||
map['imageUrl'],
|
||||
map['svgUrl'],
|
||||
map['svg'],
|
||||
map['value'],
|
||||
]);
|
||||
}
|
||||
final map = _asMap(value);
|
||||
if (map.isNotEmpty) {
|
||||
return _firstAssetString([
|
||||
map['sourceUrl'],
|
||||
map['url'],
|
||||
map['resourceUrl'],
|
||||
map['fileUrl'],
|
||||
map['src'],
|
||||
map['path'],
|
||||
map['cover'],
|
||||
map['imageUrl'],
|
||||
map['svgUrl'],
|
||||
map['svg'],
|
||||
map['value'],
|
||||
]);
|
||||
}
|
||||
if (value is Iterable && value is! String) {
|
||||
return _firstAssetString(value);
|
||||
}
|
||||
final text = _stringValue(value);
|
||||
if (text == '{}' || text == '[]' || text.toLowerCase() == 'null') {
|
||||
return '';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
int _intValue(dynamic value, {int fallback = 0}) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(_stringValue(value)) ?? fallback;
|
||||
}
|
||||
|
||||
num? _numValue(dynamic value) {
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
return num.tryParse(_stringValue(value));
|
||||
}
|
||||
|
||||
bool _boolValue(dynamic value, {bool fallback = false}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
final text = _stringValue(value).toLowerCase();
|
||||
if (text == 'true' || text == '1') {
|
||||
return true;
|
||||
}
|
||||
if (text == 'false' || text == '0') {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
@ -1,145 +1,343 @@
|
||||
/// roomId : 0
|
||||
/// date : ""
|
||||
/// level : 0
|
||||
/// levelName : ""
|
||||
/// currentEnergy : 0
|
||||
/// maxEnergy : 0
|
||||
/// energyPercent : 0.0
|
||||
/// status : ""
|
||||
/// statusDesc : ""
|
||||
/// totalContributors : 0
|
||||
/// launchTime : ""
|
||||
/// claimExpireTime : ""
|
||||
/// claimCount : 0
|
||||
/// canClaim : false
|
||||
/// hasClaimed : false
|
||||
/// myContribution : 0
|
||||
/// myContributionRate : 0.0
|
||||
|
||||
class SCRoomRocketStatusRes {
|
||||
SCRoomRocketStatusRes({
|
||||
String? roomId,
|
||||
String? date,
|
||||
num? level,
|
||||
String? levelName,
|
||||
int? currentEnergy,
|
||||
int? maxEnergy,
|
||||
num? energyPercent,
|
||||
String? status,
|
||||
String? statusDesc,
|
||||
num? totalContributors,
|
||||
String? launchTime,
|
||||
String? claimExpireTime,
|
||||
num? claimCount,
|
||||
bool? canClaim,
|
||||
bool? hasClaimed,
|
||||
num? myContribution,
|
||||
num? myContributionRate,
|
||||
}) {
|
||||
_roomId = roomId;
|
||||
_date = date;
|
||||
_level = level;
|
||||
_levelName = levelName;
|
||||
_currentEnergy = currentEnergy;
|
||||
_maxEnergy = maxEnergy;
|
||||
_energyPercent = energyPercent;
|
||||
_status = status;
|
||||
_statusDesc = statusDesc;
|
||||
_totalContributors = totalContributors;
|
||||
_claimExpireTime = claimExpireTime;
|
||||
_claimCount = claimCount;
|
||||
_canClaim = canClaim;
|
||||
_hasClaimed = hasClaimed;
|
||||
_myContribution = myContribution;
|
||||
_myContributionRate = myContributionRate;
|
||||
}
|
||||
|
||||
SCRoomRocketStatusRes.fromJson(dynamic json) {
|
||||
_roomId = json['roomId'];
|
||||
_date = json['date'];
|
||||
_level = json['level'];
|
||||
_levelName = json['levelName'];
|
||||
_currentEnergy = json['currentEnergy'];
|
||||
_maxEnergy = json['maxEnergy'];
|
||||
_energyPercent = json['energyPercent'];
|
||||
_status = json['status'];
|
||||
_statusDesc = json['statusDesc'];
|
||||
_totalContributors = json['totalContributors'];
|
||||
_claimExpireTime = json['claimExpireTime'];
|
||||
_claimCount = json['claimCount'];
|
||||
_canClaim = json['canClaim'];
|
||||
_hasClaimed = json['hasClaimed'];
|
||||
_myContribution = json['myContribution'];
|
||||
_myContributionRate = json['myContributionRate'];
|
||||
}
|
||||
|
||||
String? _roomId;
|
||||
String? _date;
|
||||
num? _level;
|
||||
String? _levelName;
|
||||
int? _currentEnergy;
|
||||
int? _maxEnergy;
|
||||
num? _energyPercent;
|
||||
String? _status;
|
||||
String? _statusDesc;
|
||||
num? _totalContributors;
|
||||
String? _claimExpireTime;
|
||||
num? _claimCount;
|
||||
bool? _canClaim;
|
||||
bool? _hasClaimed;
|
||||
num? _myContribution;
|
||||
num? _myContributionRate;
|
||||
|
||||
String? get roomId => _roomId;
|
||||
|
||||
String? get date => _date;
|
||||
|
||||
num? get level => _level;
|
||||
|
||||
String? get levelName => _levelName;
|
||||
|
||||
int? get currentEnergy => _currentEnergy;
|
||||
|
||||
int? get maxEnergy => _maxEnergy;
|
||||
|
||||
num? get energyPercent => _energyPercent;
|
||||
|
||||
String? get status => _status;
|
||||
|
||||
String? get statusDesc => _statusDesc;
|
||||
|
||||
num? get totalContributors => _totalContributors;
|
||||
|
||||
String? get claimExpireTime => _claimExpireTime;
|
||||
|
||||
num? get claimCount => _claimCount;
|
||||
|
||||
bool? get canClaim => _canClaim;
|
||||
|
||||
bool? get hasClaimed => _hasClaimed;
|
||||
|
||||
num? get myContribution => _myContribution;
|
||||
|
||||
num? get myContributionRate => _myContributionRate;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['roomId'] = _roomId;
|
||||
map['date'] = _date;
|
||||
map['level'] = _level;
|
||||
map['levelName'] = _levelName;
|
||||
map['currentEnergy'] = _currentEnergy;
|
||||
map['maxEnergy'] = _maxEnergy;
|
||||
map['energyPercent'] = _energyPercent;
|
||||
map['status'] = _status;
|
||||
map['statusDesc'] = _statusDesc;
|
||||
map['totalContributors'] = _totalContributors;
|
||||
map['claimExpireTime'] = _claimExpireTime;
|
||||
map['claimCount'] = _claimCount;
|
||||
map['canClaim'] = _canClaim;
|
||||
map['hasClaimed'] = _hasClaimed;
|
||||
map['myContribution'] = _myContribution;
|
||||
map['myContributionRate'] = _myContributionRate;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
|
||||
/// roomId : 0
|
||||
/// date : ""
|
||||
/// level : 0
|
||||
/// levelName : ""
|
||||
/// currentEnergy : 0
|
||||
/// maxEnergy : 0
|
||||
/// energyPercent : 0.0
|
||||
/// status : ""
|
||||
/// statusDesc : ""
|
||||
/// totalContributors : 0
|
||||
/// launchTime : ""
|
||||
/// claimExpireTime : ""
|
||||
/// claimCount : 0
|
||||
/// canClaim : false
|
||||
/// hasClaimed : false
|
||||
/// myContribution : 0
|
||||
/// myContributionRate : 0.0
|
||||
|
||||
class SCRoomRocketStatusRes {
|
||||
SCRoomRocketStatusRes({
|
||||
String? roomId,
|
||||
String? date,
|
||||
num? level,
|
||||
String? levelName,
|
||||
int? currentEnergy,
|
||||
int? maxEnergy,
|
||||
num? energyPercent,
|
||||
String? status,
|
||||
String? statusDesc,
|
||||
num? totalContributors,
|
||||
String? launchTime,
|
||||
String? claimExpireTime,
|
||||
num? claimCount,
|
||||
bool? canClaim,
|
||||
bool? hasClaimed,
|
||||
num? myContribution,
|
||||
num? myContributionRate,
|
||||
int? roundNo,
|
||||
bool? displayRound,
|
||||
int? currentLevel,
|
||||
int? needEnergy,
|
||||
num? displayPercent,
|
||||
bool? shake,
|
||||
List<SCRoomRocketLevelRes>? levels,
|
||||
SCRoomRocketRewardPreviewRes? rewardPreview,
|
||||
List<SCRoomRocketKingUserRes>? rocketKings,
|
||||
String? timezone,
|
||||
int? resetRemainingSeconds,
|
||||
Object? resetTime,
|
||||
Object? serverTime,
|
||||
}) {
|
||||
_roomId = roomId;
|
||||
_date = date;
|
||||
_level = level;
|
||||
_levelName = levelName;
|
||||
_currentEnergy = currentEnergy;
|
||||
_maxEnergy = maxEnergy;
|
||||
_energyPercent = energyPercent;
|
||||
_status = status;
|
||||
_statusDesc = statusDesc;
|
||||
_totalContributors = totalContributors;
|
||||
_claimExpireTime = claimExpireTime;
|
||||
_claimCount = claimCount;
|
||||
_canClaim = canClaim;
|
||||
_hasClaimed = hasClaimed;
|
||||
_myContribution = myContribution;
|
||||
_myContributionRate = myContributionRate;
|
||||
_roundNo = roundNo;
|
||||
_displayRound = displayRound;
|
||||
_currentLevel = currentLevel;
|
||||
_needEnergy = needEnergy;
|
||||
_displayPercent = displayPercent;
|
||||
_shake = shake;
|
||||
_levels = levels;
|
||||
_rewardPreview = rewardPreview;
|
||||
_rocketKings = rocketKings;
|
||||
_timezone = timezone;
|
||||
_resetRemainingSeconds = resetRemainingSeconds;
|
||||
_resetTime = resetTime;
|
||||
_serverTime = serverTime;
|
||||
}
|
||||
|
||||
SCRoomRocketStatusRes.fromJson(dynamic json) {
|
||||
_roomId = json['roomId'];
|
||||
_date = json['date'] ?? json['dayKey'];
|
||||
_roundNo = _intValue(json['roundNo']);
|
||||
_displayRound = _boolValue(json['displayRound']);
|
||||
_currentLevel = _intValue(json['currentLevel']);
|
||||
_needEnergy = _intValue(json['needEnergy']);
|
||||
_displayPercent = _numValue(json['displayPercent']);
|
||||
_shake = _boolValue(json['shake']);
|
||||
_level = json['level'] ?? _currentLevel;
|
||||
_levelName = json['levelName'];
|
||||
_currentEnergy = _intValue(json['currentEnergy']);
|
||||
_maxEnergy = _intValue(json['maxEnergy']) ?? _needEnergy;
|
||||
_energyPercent = _numValue(json['energyPercent']) ?? _displayPercent;
|
||||
_status = json['status'];
|
||||
_statusDesc = json['statusDesc'];
|
||||
_rocketKings = _kingList(json['rocketKings']);
|
||||
_totalContributors =
|
||||
json['totalContributors'] ?? (_rocketKings?.length ?? 0);
|
||||
_claimExpireTime = json['claimExpireTime'];
|
||||
_claimCount = json['claimCount'];
|
||||
_canClaim = json['canClaim'];
|
||||
_hasClaimed = json['hasClaimed'];
|
||||
_myContribution = json['myContribution'];
|
||||
_myContributionRate = json['myContributionRate'];
|
||||
_levels = _levelList(json['levels']);
|
||||
_rewardPreview =
|
||||
json['rewardPreview'] == null
|
||||
? null
|
||||
: SCRoomRocketRewardPreviewRes.fromJson(json['rewardPreview']);
|
||||
_timezone = _stringValue(json['timezone'] ?? json['timeZone']);
|
||||
final resetRemainingMillis = _intValue(
|
||||
json['resetRemainingMillis'] ??
|
||||
json['resetRemainMillis'] ??
|
||||
json['remainingMillis'] ??
|
||||
json['remainMillis'],
|
||||
);
|
||||
_resetRemainingSeconds =
|
||||
_intValue(
|
||||
json['resetRemainingSeconds'] ??
|
||||
json['resetRemainSeconds'] ??
|
||||
json['remainingSeconds'] ??
|
||||
json['remainSeconds'] ??
|
||||
json['countdownSeconds'] ??
|
||||
json['resetCountdownSeconds'] ??
|
||||
json['secondsToReset'] ??
|
||||
json['resetSeconds'],
|
||||
) ??
|
||||
(resetRemainingMillis == null
|
||||
? null
|
||||
: (resetRemainingMillis / 1000).ceil());
|
||||
_resetTime = _firstNonNull([
|
||||
json['resetTime'],
|
||||
json['resetAt'],
|
||||
json['resetDateTime'],
|
||||
json['resetTimestamp'],
|
||||
json['resetTimeMillis'],
|
||||
json['nextResetTime'],
|
||||
json['nextResetAt'],
|
||||
json['nextResetDateTime'],
|
||||
json['nextResetTimestamp'],
|
||||
json['nextResetTimeMillis'],
|
||||
json['dayEndTime'],
|
||||
json['nextDayTime'],
|
||||
]);
|
||||
_serverTime = _firstNonNull([
|
||||
json['serverTime'],
|
||||
json['currentTime'],
|
||||
json['currentTimestamp'],
|
||||
json['now'],
|
||||
json['time'],
|
||||
]);
|
||||
}
|
||||
|
||||
String? _roomId;
|
||||
String? _date;
|
||||
num? _level;
|
||||
String? _levelName;
|
||||
int? _currentEnergy;
|
||||
int? _maxEnergy;
|
||||
num? _energyPercent;
|
||||
String? _status;
|
||||
String? _statusDesc;
|
||||
num? _totalContributors;
|
||||
String? _claimExpireTime;
|
||||
num? _claimCount;
|
||||
bool? _canClaim;
|
||||
bool? _hasClaimed;
|
||||
num? _myContribution;
|
||||
num? _myContributionRate;
|
||||
int? _roundNo;
|
||||
bool? _displayRound;
|
||||
int? _currentLevel;
|
||||
int? _needEnergy;
|
||||
num? _displayPercent;
|
||||
bool? _shake;
|
||||
List<SCRoomRocketLevelRes>? _levels;
|
||||
SCRoomRocketRewardPreviewRes? _rewardPreview;
|
||||
List<SCRoomRocketKingUserRes>? _rocketKings;
|
||||
String? _timezone;
|
||||
int? _resetRemainingSeconds;
|
||||
Object? _resetTime;
|
||||
Object? _serverTime;
|
||||
|
||||
String? get roomId => _roomId;
|
||||
|
||||
String? get date => _date;
|
||||
|
||||
num? get level => _level;
|
||||
|
||||
String? get levelName => _levelName;
|
||||
|
||||
int? get currentEnergy => _currentEnergy;
|
||||
|
||||
int? get maxEnergy => _maxEnergy;
|
||||
|
||||
num? get energyPercent => _energyPercent;
|
||||
|
||||
String? get status => _status;
|
||||
|
||||
String? get statusDesc => _statusDesc;
|
||||
|
||||
num? get totalContributors => _totalContributors;
|
||||
|
||||
String? get claimExpireTime => _claimExpireTime;
|
||||
|
||||
num? get claimCount => _claimCount;
|
||||
|
||||
bool? get canClaim => _canClaim;
|
||||
|
||||
bool? get hasClaimed => _hasClaimed;
|
||||
|
||||
num? get myContribution => _myContribution;
|
||||
|
||||
num? get myContributionRate => _myContributionRate;
|
||||
|
||||
int? get roundNo => _roundNo;
|
||||
|
||||
bool? get displayRound => _displayRound;
|
||||
|
||||
int? get currentLevel => _currentLevel;
|
||||
|
||||
int? get needEnergy => _needEnergy;
|
||||
|
||||
num? get displayPercent => _displayPercent;
|
||||
|
||||
bool? get shake => _shake;
|
||||
|
||||
List<SCRoomRocketLevelRes>? get levels => _levels;
|
||||
|
||||
SCRoomRocketRewardPreviewRes? get rewardPreview => _rewardPreview;
|
||||
|
||||
List<SCRoomRocketKingUserRes>? get rocketKings => _rocketKings;
|
||||
|
||||
String? get timezone => _timezone;
|
||||
|
||||
int? get resetRemainingSeconds => _resetRemainingSeconds;
|
||||
|
||||
Object? get resetTime => _resetTime;
|
||||
|
||||
Object? get serverTime => _serverTime;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['roomId'] = _roomId;
|
||||
map['date'] = _date;
|
||||
map['level'] = _level;
|
||||
map['levelName'] = _levelName;
|
||||
map['currentEnergy'] = _currentEnergy;
|
||||
map['maxEnergy'] = _maxEnergy;
|
||||
map['energyPercent'] = _energyPercent;
|
||||
map['status'] = _status;
|
||||
map['statusDesc'] = _statusDesc;
|
||||
map['totalContributors'] = _totalContributors;
|
||||
map['claimExpireTime'] = _claimExpireTime;
|
||||
map['claimCount'] = _claimCount;
|
||||
map['canClaim'] = _canClaim;
|
||||
map['hasClaimed'] = _hasClaimed;
|
||||
map['myContribution'] = _myContribution;
|
||||
map['myContributionRate'] = _myContributionRate;
|
||||
map['roundNo'] = _roundNo;
|
||||
map['displayRound'] = _displayRound;
|
||||
map['currentLevel'] = _currentLevel;
|
||||
map['needEnergy'] = _needEnergy;
|
||||
map['displayPercent'] = _displayPercent;
|
||||
map['shake'] = _shake;
|
||||
map['timezone'] = _timezone;
|
||||
map['resetRemainingSeconds'] = _resetRemainingSeconds;
|
||||
map['resetTime'] = _resetTime;
|
||||
map['serverTime'] = _serverTime;
|
||||
return map;
|
||||
}
|
||||
|
||||
static int? _intValue(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(value?.toString().trim() ?? '');
|
||||
}
|
||||
|
||||
static num? _numValue(dynamic value) {
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
return num.tryParse(value?.toString().trim() ?? '');
|
||||
}
|
||||
|
||||
static bool? _boolValue(dynamic value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
final text = value?.toString().trim().toLowerCase();
|
||||
if (text == 'true' || text == '1') {
|
||||
return true;
|
||||
}
|
||||
if (text == 'false' || text == '0') {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _stringValue(dynamic value) {
|
||||
final text = value?.toString().trim();
|
||||
if (text == null || text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static Object? _firstNonNull(List<Object?> values) {
|
||||
for (final value in values) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
if (value is String && value.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SCRoomRocketLevelRes>? _levelList(dynamic value) {
|
||||
if (value is! Iterable) {
|
||||
return null;
|
||||
}
|
||||
return value.map(SCRoomRocketLevelRes.fromJson).toList(growable: false);
|
||||
}
|
||||
|
||||
static List<SCRoomRocketKingUserRes>? _kingList(dynamic value) {
|
||||
if (value is! Iterable) {
|
||||
return null;
|
||||
}
|
||||
return value.map(SCRoomRocketKingUserRes.fromJson).toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
76
lib/shared/business_logic/models/res/sc_user_badge_res.dart
Normal file
@ -0,0 +1,76 @@
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
|
||||
class SCUserBadgeRes {
|
||||
SCUserBadgeRes({
|
||||
this.userId,
|
||||
List<WearBadge>? badges,
|
||||
List<WearBadge>? longBadges,
|
||||
List<WearBadge>? shortBadges,
|
||||
}) : badges = badges ?? const <WearBadge>[],
|
||||
longBadges = longBadges ?? const <WearBadge>[],
|
||||
shortBadges = shortBadges ?? const <WearBadge>[];
|
||||
|
||||
factory SCUserBadgeRes.empty({String? userId}) {
|
||||
return SCUserBadgeRes(userId: userId);
|
||||
}
|
||||
|
||||
factory SCUserBadgeRes.fromJson(dynamic json) {
|
||||
final source = _bodyFrom(json);
|
||||
if (source is! Map) {
|
||||
return SCUserBadgeRes.empty();
|
||||
}
|
||||
return SCUserBadgeRes(
|
||||
userId: _stringValue(source['userId']),
|
||||
badges: _badgeList(source['badges']),
|
||||
longBadges: _badgeList(source['longBadges']),
|
||||
shortBadges: _badgeList(source['shortBadges']),
|
||||
);
|
||||
}
|
||||
|
||||
final String? userId;
|
||||
final List<WearBadge> badges;
|
||||
final List<WearBadge> longBadges;
|
||||
final List<WearBadge> shortBadges;
|
||||
|
||||
List<WearBadge> get displayBadges {
|
||||
if (longBadges.isNotEmpty || shortBadges.isNotEmpty) {
|
||||
return <WearBadge>[...longBadges, ...shortBadges];
|
||||
}
|
||||
return badges;
|
||||
}
|
||||
|
||||
static dynamic _bodyFrom(dynamic json) {
|
||||
if (json is Map) {
|
||||
final body = json['body'];
|
||||
if (body is Map) {
|
||||
return body;
|
||||
}
|
||||
final data = json['data'];
|
||||
if (data is Map) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
static List<WearBadge> _badgeList(dynamic value) {
|
||||
if (value is! List) {
|
||||
return const <WearBadge>[];
|
||||
}
|
||||
return value
|
||||
.whereType<Map>()
|
||||
.map((item) => WearBadge.fromJson(item))
|
||||
.where((badge) => _badgeUrl(badge).isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static String _badgeUrl(WearBadge badge) {
|
||||
return (badge.selectUrl ?? badge.notSelectUrl ?? badge.animationUrl ?? "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
static String? _stringValue(dynamic value) {
|
||||
final text = value?.toString().trim();
|
||||
return text == null || text.isEmpty ? null : text;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
/// agent : true
|
||||
/// anchor : true
|
||||
/// bd : false
|
||||
/// freightAgent : false
|
||||
// agent : true
|
||||
// anchor : true
|
||||
// bd : false
|
||||
// freightAgent : false
|
||||
|
||||
class SCUserIdentityRes {
|
||||
SCUserIdentityRes({
|
||||
@ -13,12 +13,14 @@ class SCUserIdentityRes {
|
||||
bool? bdLeader,
|
||||
bool? superFreightAgent,
|
||||
bool? manager,
|
||||
bool? yumiManager,
|
||||
bool? freightAgent,}){
|
||||
_agent = agent;
|
||||
_superFreightAgent = superFreightAgent;
|
||||
_anchor = anchor;
|
||||
_bd = bd;
|
||||
_manager = manager;
|
||||
_yumiManager = yumiManager;
|
||||
_admin = admin;
|
||||
_superAdmin = superAdmin;
|
||||
_bdLeader = bdLeader;
|
||||
@ -35,6 +37,9 @@ class SCUserIdentityRes {
|
||||
_admin = json['admin'];
|
||||
_freightAgent = json['freightAgent'];
|
||||
_manager = json['manager'];
|
||||
_yumiManager = _boolValue(
|
||||
json['yumi_manager'] ?? json['yumiManager'] ?? json['manager'],
|
||||
);
|
||||
}
|
||||
bool? _agent;
|
||||
bool? _anchor;
|
||||
@ -44,13 +49,16 @@ class SCUserIdentityRes {
|
||||
bool? _bdLeader;
|
||||
bool? _freightAgent;
|
||||
bool? _manager;
|
||||
bool? _yumiManager;
|
||||
bool? _superFreightAgent;
|
||||
SCUserIdentityRes copyWith({ bool? agent,
|
||||
bool? anchor,
|
||||
bool? bd,
|
||||
bool? admin,
|
||||
bool? superAdmin,
|
||||
bool? bdLeader,
|
||||
bool? manager,
|
||||
bool? yumiManager,
|
||||
bool? superFreightAgent,
|
||||
bool? freightAgent,
|
||||
}) => SCUserIdentityRes( agent: agent ?? _agent,
|
||||
@ -60,6 +68,7 @@ SCUserIdentityRes copyWith({ bool? agent,
|
||||
bdLeader: bdLeader ?? _bdLeader,
|
||||
admin: admin ?? _admin,
|
||||
manager: manager ?? _manager,
|
||||
yumiManager: yumiManager ?? _yumiManager,
|
||||
superFreightAgent: superFreightAgent ?? _superFreightAgent,
|
||||
freightAgent: freightAgent ?? _freightAgent,
|
||||
);
|
||||
@ -68,6 +77,7 @@ SCUserIdentityRes copyWith({ bool? agent,
|
||||
bool? get bd => _bd;
|
||||
bool? get admin => _admin;
|
||||
bool? get manager => _manager;
|
||||
bool? get yumiManager => _yumiManager;
|
||||
bool? get superAdmin => _superAdmin;
|
||||
bool? get bdLeader => _bdLeader;
|
||||
bool? get superFreightAgent => _superFreightAgent;
|
||||
@ -79,6 +89,7 @@ SCUserIdentityRes copyWith({ bool? agent,
|
||||
map['anchor'] = _anchor;
|
||||
map['bd'] = _bd;
|
||||
map['manager'] = _manager;
|
||||
map['yumi_manager'] = _yumiManager;
|
||||
map['admin'] = _admin;
|
||||
map['superAdmin'] = _superAdmin;
|
||||
map['bdLeader'] = _bdLeader;
|
||||
@ -87,4 +98,25 @@ SCUserIdentityRes copyWith({ bool? agent,
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
static bool? _boolValue(dynamic value) {
|
||||
if (value == null || value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
if (value is String) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
if (normalized.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (normalized == 'true' || normalized == '1' || normalized == 'yes') {
|
||||
return true;
|
||||
}
|
||||
if (normalized == 'false' || normalized == '0' || normalized == 'no') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_sent_re
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_reward_info_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_task_claimable_count_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_task_list_res.dart';
|
||||
@ -189,9 +190,49 @@ abstract class SocialChatRoomRepository {
|
||||
///礼物背包
|
||||
Future<List<SocialChatGiftBackpackRes>> giftBackpack();
|
||||
|
||||
///赠送背包礼物
|
||||
Future<Object?> giveBackpackGift({
|
||||
required String id,
|
||||
required String acceptUserId,
|
||||
required String giftId,
|
||||
required num quantity,
|
||||
String? roomId,
|
||||
num? seatIndex,
|
||||
String? songId,
|
||||
});
|
||||
|
||||
///查询房间火箭状态
|
||||
Future<SCRoomRocketStatusRes> rocketStatus(String roomId);
|
||||
|
||||
///查询火箭王榜单
|
||||
Future<SCRoomRocketKingRes> roomRocketKing(
|
||||
String roomId, {
|
||||
int level = 1,
|
||||
int roundNo = 0,
|
||||
int cursor = 1,
|
||||
int limit = 100,
|
||||
});
|
||||
|
||||
///查询中奖弹窗
|
||||
Future<SCRoomRocketRewardPopupRes> roomRocketRewardPopups(
|
||||
String roomId, {
|
||||
int cursor = 1,
|
||||
int limit = 10,
|
||||
});
|
||||
|
||||
///确认中奖弹窗已读
|
||||
Future<bool> ackRoomRocketRewardPopups(List<String> rewardRecordIds);
|
||||
|
||||
///查询我的中奖记录
|
||||
Future<SCRoomRocketRewardRecordsRes> roomRocketRewardRecords(
|
||||
String roomId, {
|
||||
int cursor = 1,
|
||||
int limit = 10,
|
||||
});
|
||||
|
||||
///查询火箭规则说明
|
||||
Future<dynamic> roomRocketRule();
|
||||
|
||||
///查询火箭配置
|
||||
Future<List<SCRoomRocketConfigRes>> rocketConfigEnabled();
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_reward_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart';
|
||||
@ -63,7 +64,13 @@ abstract class SocialChatUserRepository {
|
||||
Future<SocialChatRoomRes> createRoom();
|
||||
|
||||
///加入房间
|
||||
Future<JoinRoomRes> entryRoom(String roomId, {String? pwd});
|
||||
Future<JoinRoomRes> entryRoom(
|
||||
String roomId, {
|
||||
String? pwd,
|
||||
String redPackId = "",
|
||||
bool needOpenRedenvelope = false,
|
||||
bool silentErrorToast = false,
|
||||
});
|
||||
|
||||
///发送心跳
|
||||
Future heartbeat(String status, bool upMick, {String? roomId});
|
||||
@ -248,6 +255,12 @@ abstract class SocialChatUserRepository {
|
||||
///查询已拥有徽章
|
||||
Future<List<WearBadge>> badgeOwnList(String type);
|
||||
|
||||
///查询当前用户正在展示的徽章
|
||||
Future<SCUserBadgeRes> currentUserBadges();
|
||||
|
||||
///查询其他用户正在展示的徽章
|
||||
Future<SCUserBadgeRes> otherUserBadges(String userId);
|
||||
|
||||
///切换佩戴徽章
|
||||
Future<bool> badgeToggle(String badgeId);
|
||||
|
||||
|
||||
@ -0,0 +1,197 @@
|
||||
class RoomRocketLaunchBroadcastMessage {
|
||||
const RoomRocketLaunchBroadcastMessage({
|
||||
required this.roomId,
|
||||
required this.roomAccount,
|
||||
required this.roomName,
|
||||
required this.roomCoverUrl,
|
||||
required this.roundNo,
|
||||
required this.level,
|
||||
required this.rocketIconUrl,
|
||||
required this.rocketAnimationUrl,
|
||||
required this.durationSeconds,
|
||||
required this.triggerUser,
|
||||
required this.triggerUserId,
|
||||
required this.launchNo,
|
||||
});
|
||||
|
||||
factory RoomRocketLaunchBroadcastMessage.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
return RoomRocketLaunchBroadcastMessage(
|
||||
roomId: _stringValue(data['roomId']),
|
||||
roomAccount: _stringValue(data['roomAccount']),
|
||||
roomName: _stringValue(data['roomName']),
|
||||
roomCoverUrl: _roomCoverUrl(data),
|
||||
roundNo: _intValue(data['roundNo']),
|
||||
level: _intValue(data['level'], fallback: 1),
|
||||
rocketIconUrl: _rocketIconUrl(data),
|
||||
rocketAnimationUrl: _rocketAnimationUrl(data),
|
||||
durationSeconds: _intValue(data['durationSeconds'], fallback: 7),
|
||||
triggerUser: RoomRocketLaunchTriggerUser.fromJson(data['triggerUser']),
|
||||
triggerUserId: _stringValue(data['triggerUserId']),
|
||||
launchNo: _stringValue(data['launchNo']),
|
||||
);
|
||||
}
|
||||
|
||||
final String roomId;
|
||||
final String roomAccount;
|
||||
final String roomName;
|
||||
final String roomCoverUrl;
|
||||
final int roundNo;
|
||||
final int level;
|
||||
final String rocketIconUrl;
|
||||
final String rocketAnimationUrl;
|
||||
final int durationSeconds;
|
||||
final RoomRocketLaunchTriggerUser triggerUser;
|
||||
final String triggerUserId;
|
||||
final String launchNo;
|
||||
|
||||
bool get isValid => roomId.isNotEmpty;
|
||||
|
||||
int get safeLevel {
|
||||
if (level < 1) {
|
||||
return 1;
|
||||
}
|
||||
if (level > 6) {
|
||||
return 6;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
Duration get displayDuration {
|
||||
if (durationSeconds <= 0) {
|
||||
return const Duration(seconds: 7);
|
||||
}
|
||||
return Duration(seconds: durationSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomRocketLaunchTriggerUser {
|
||||
const RoomRocketLaunchTriggerUser({
|
||||
required this.userId,
|
||||
required this.nickname,
|
||||
required this.avatar,
|
||||
required this.account,
|
||||
required this.countryCode,
|
||||
required this.countryName,
|
||||
});
|
||||
|
||||
factory RoomRocketLaunchTriggerUser.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
return RoomRocketLaunchTriggerUser(
|
||||
userId: _stringValue(data['userId']),
|
||||
nickname: _stringValue(data['nickname']),
|
||||
avatar: _stringValue(data['avatar']),
|
||||
account: _stringValue(data['account']),
|
||||
countryCode: _stringValue(data['countryCode']),
|
||||
countryName: _stringValue(data['countryName']),
|
||||
);
|
||||
}
|
||||
|
||||
final String userId;
|
||||
final String nickname;
|
||||
final String avatar;
|
||||
final String account;
|
||||
final String countryCode;
|
||||
final String countryName;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _asMap(dynamic value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
return value;
|
||||
}
|
||||
if (value is Map) {
|
||||
return value.map((key, item) => MapEntry(key.toString(), item));
|
||||
}
|
||||
return const <String, dynamic>{};
|
||||
}
|
||||
|
||||
String _stringValue(dynamic value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return value.toString().trim();
|
||||
}
|
||||
|
||||
String _roomCoverUrl(Map<String, dynamic> data) {
|
||||
return _firstStringValue([
|
||||
data['roomCoverUrl'],
|
||||
data['roomCover'],
|
||||
data['roomAvatar'],
|
||||
data['roomIcon'],
|
||||
data['coverUrl'],
|
||||
data['cover'],
|
||||
_asMap(data['room'])['coverUrl'],
|
||||
_asMap(data['room'])['cover'],
|
||||
_asMap(data['room'])['avatar'],
|
||||
]);
|
||||
}
|
||||
|
||||
String _rocketIconUrl(Map<String, dynamic> data) {
|
||||
return _firstStringValue([
|
||||
data['rocket_icon_url'],
|
||||
data['rocketIconUrl'],
|
||||
data['rocketIcon'],
|
||||
data['rocketUrl'],
|
||||
data['rocketImageUrl'],
|
||||
data['rocketImage'],
|
||||
data['iconUrl'],
|
||||
_asMap(data['rocket'])['rocket_icon_url'],
|
||||
_asMap(data['rocket'])['rocketIconUrl'],
|
||||
_asMap(data['rocket'])['iconUrl'],
|
||||
_asMap(data['rocket'])['icon'],
|
||||
_asMap(data['levelInfo'])['rocket_icon_url'],
|
||||
_asMap(data['levelInfo'])['rocketIconUrl'],
|
||||
_asMap(data['rocketLevel'])['rocket_icon_url'],
|
||||
_asMap(data['rocketLevel'])['rocketIconUrl'],
|
||||
_asMap(data['config'])['rocket_icon_url'],
|
||||
_asMap(data['config'])['rocketIconUrl'],
|
||||
]);
|
||||
}
|
||||
|
||||
String _rocketAnimationUrl(Map<String, dynamic> data) {
|
||||
return _firstStringValue([
|
||||
data['rocket_animation_url'],
|
||||
data['rocketAnimationUrl'],
|
||||
data['rocketSvgaUrl'],
|
||||
data['animationUrl'],
|
||||
data['svgaUrl'],
|
||||
data['rocketLaunchSvgaUrl'],
|
||||
data['launchAnimationUrl'],
|
||||
data['launchSvgaUrl'],
|
||||
data['sourceUrl'],
|
||||
_asMap(data['rocket'])['rocket_animation_url'],
|
||||
_asMap(data['rocket'])['rocketAnimationUrl'],
|
||||
_asMap(data['rocket'])['rocketSvgaUrl'],
|
||||
_asMap(data['rocket'])['animationUrl'],
|
||||
_asMap(data['rocket'])['svgaUrl'],
|
||||
_asMap(data['levelInfo'])['rocket_animation_url'],
|
||||
_asMap(data['levelInfo'])['rocketAnimationUrl'],
|
||||
_asMap(data['levelInfo'])['rocketSvgaUrl'],
|
||||
_asMap(data['rocketLevel'])['rocket_animation_url'],
|
||||
_asMap(data['rocketLevel'])['rocketAnimationUrl'],
|
||||
_asMap(data['rocketLevel'])['rocketSvgaUrl'],
|
||||
_asMap(data['config'])['rocket_animation_url'],
|
||||
_asMap(data['config'])['rocketAnimationUrl'],
|
||||
_asMap(data['config'])['rocketSvgaUrl'],
|
||||
]);
|
||||
}
|
||||
|
||||
String _firstStringValue(Iterable<dynamic> values) {
|
||||
for (final value in values) {
|
||||
final text = _stringValue(value);
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
int _intValue(dynamic value, {int fallback = 0}) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(_stringValue(value)) ?? fallback;
|
||||
}
|
||||
@ -16,6 +16,7 @@ class SCFloatingMessage {
|
||||
num? coins;
|
||||
num? number;
|
||||
num? multiple;
|
||||
num? durationSeconds;
|
||||
String? broadcastScope; // region:来自语区 IM 群,只能按区域飘屏规则展示
|
||||
int priority = 10; //排序权重
|
||||
|
||||
@ -34,6 +35,7 @@ class SCFloatingMessage {
|
||||
this.giftId = '',
|
||||
this.number = 0,
|
||||
this.coins = 0,
|
||||
this.durationSeconds,
|
||||
this.priority = 10,
|
||||
this.multiple = 10,
|
||||
this.broadcastScope = '',
|
||||
@ -54,6 +56,7 @@ class SCFloatingMessage {
|
||||
giftId = json['giftId'];
|
||||
coins = json['coins'];
|
||||
number = json['number'];
|
||||
durationSeconds = json['durationSeconds'];
|
||||
priority = json['priority'];
|
||||
multiple = json['multiple'];
|
||||
broadcastScope = json['broadcastScope'];
|
||||
@ -77,6 +80,7 @@ class SCFloatingMessage {
|
||||
map['giftId'] = giftId;
|
||||
map['coins'] = coins;
|
||||
map['number'] = number;
|
||||
map['durationSeconds'] = durationSeconds;
|
||||
map['priority'] = priority;
|
||||
map['multiple'] = multiple;
|
||||
map['broadcastScope'] = broadcastScope;
|
||||
|
||||
@ -27,6 +27,9 @@ class OverlayManager {
|
||||
static const int _maxDeferredRegionRedPacketCount = 12;
|
||||
static const Duration _compensationDedupWindow = Duration(seconds: 3);
|
||||
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
|
||||
static const List<int> _stackedFloatingOrder = [5, 1, 4, 3];
|
||||
static const double _stackedFloatingBaseTop = 70;
|
||||
static const double _stackedFloatingGap = 8;
|
||||
|
||||
final SCPriorityQueue<SCFloatingMessage> _messageQueue = SCPriorityQueue(
|
||||
(a, b) => b.priority.compareTo(a.priority),
|
||||
@ -34,6 +37,8 @@ class OverlayManager {
|
||||
final List<SCFloatingMessage> _deferredRegionRedPacketMessages = [];
|
||||
final Map<String, DateTime> _recentCompensationKeys = {};
|
||||
final Map<String, DateTime> _recentFloatingKeys = {};
|
||||
final Map<int, OverlayEntry> _stackedOverlayEntries = {};
|
||||
final Map<int, SCFloatingMessage> _stackedMessages = {};
|
||||
bool _isPlaying = false;
|
||||
OverlayEntry? _currentOverlayEntry;
|
||||
SCFloatingMessage? _currentMessage;
|
||||
@ -55,7 +60,6 @@ class OverlayManager {
|
||||
if (_isRecentFloatingDuplicate(message)) {
|
||||
return;
|
||||
}
|
||||
if (message.type == 4) {}
|
||||
_rememberFloatingMessage(message);
|
||||
_enqueueMessage(
|
||||
message,
|
||||
@ -63,6 +67,7 @@ class OverlayManager {
|
||||
);
|
||||
} else {
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
_removeAllStackedMessages();
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
_isPlaying = false;
|
||||
@ -162,14 +167,21 @@ class OverlayManager {
|
||||
}
|
||||
|
||||
String? _floatingDedupKeyFor(SCFloatingMessage message) {
|
||||
if (message.type != 4) {
|
||||
return null;
|
||||
if (message.type == 3) {
|
||||
final launchNo = message.giftId?.trim() ?? '';
|
||||
if (launchNo.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return 'room_rocket_launch|$launchNo';
|
||||
}
|
||||
final packetId = message.toUserId?.trim() ?? '';
|
||||
if (packetId.isEmpty) {
|
||||
return null;
|
||||
if (message.type == 4) {
|
||||
final packetId = message.toUserId?.trim() ?? '';
|
||||
if (packetId.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return 'room_red_packet|$packetId';
|
||||
}
|
||||
return 'room_red_packet|$packetId';
|
||||
return null;
|
||||
}
|
||||
|
||||
String _compensationKeyFor(SCFloatingMessage message) {
|
||||
@ -231,12 +243,14 @@ class OverlayManager {
|
||||
await SCSvgaAssetWidget.preloadAsset(
|
||||
FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath,
|
||||
);
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
// Preload failure is non-fatal; the banner will retry during rendering.
|
||||
}
|
||||
}
|
||||
|
||||
void _safeScheduleNext() {
|
||||
if (_isSuppressed) return;
|
||||
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return;
|
||||
if (_isProcessing || _messageQueue.isEmpty) return;
|
||||
_isProcessing = true;
|
||||
|
||||
try {
|
||||
@ -247,7 +261,7 @@ class OverlayManager {
|
||||
}
|
||||
|
||||
void _scheduleNext() {
|
||||
if (_isPlaying || _messageQueue.isEmpty || _isDisposed || _isSuppressed) {
|
||||
if (_messageQueue.isEmpty || _isDisposed || _isSuppressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -255,37 +269,64 @@ class OverlayManager {
|
||||
final context = navigatorKey.currentState?.context;
|
||||
if (context == null || !context.mounted) return;
|
||||
|
||||
// 安全地获取第一个消息
|
||||
if (_messageQueue.isEmpty) return;
|
||||
final messageToProcess = _messageQueue.first;
|
||||
if (messageToProcess == null) {
|
||||
_messageQueue.removeFirst();
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_shouldDisplayMessage(context, messageToProcess)) {
|
||||
_messageQueue.removeFirst();
|
||||
if (_shouldDeferRegionRedPacketMessage(context, messageToProcess)) {
|
||||
_deferRegionRedPacketMessage(context, messageToProcess);
|
||||
_safeScheduleNext();
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
final messageToPlay = _removeNextPlayableMessage(context);
|
||||
if (messageToPlay == null) {
|
||||
return;
|
||||
}
|
||||
_playMessage(messageToPlay);
|
||||
if (!_isStackedFloatingMessage(messageToPlay)) {
|
||||
return;
|
||||
}
|
||||
_logRegionRedPacketDrop(context, messageToProcess);
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
|
||||
// 安全地移除并播放消息
|
||||
final messageToPlay = _messageQueue.removeFirst();
|
||||
_playMessage(messageToPlay);
|
||||
} catch (e) {
|
||||
_isPlaying = false;
|
||||
_safeScheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
SCFloatingMessage? _removeNextPlayableMessage(BuildContext context) {
|
||||
final skippedMessages = <SCFloatingMessage>[];
|
||||
SCFloatingMessage? selectedMessage;
|
||||
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
final message = _messageQueue.removeFirst();
|
||||
if (!_shouldDisplayMessage(context, message)) {
|
||||
if (_shouldDeferRegionRedPacketMessage(context, message)) {
|
||||
_deferRegionRedPacketMessage(context, message);
|
||||
continue;
|
||||
}
|
||||
_logRegionRedPacketDrop(context, message);
|
||||
continue;
|
||||
}
|
||||
if (!_canPlayMessage(message)) {
|
||||
skippedMessages.add(message);
|
||||
continue;
|
||||
}
|
||||
selectedMessage = message;
|
||||
break;
|
||||
}
|
||||
|
||||
for (final message in skippedMessages) {
|
||||
_messageQueue.add(message);
|
||||
}
|
||||
return selectedMessage;
|
||||
}
|
||||
|
||||
bool _canPlayMessage(SCFloatingMessage message) {
|
||||
final slotKey = _stackSlotKeyFor(message);
|
||||
if (slotKey != null) {
|
||||
return !_isPlaying && !_stackedOverlayEntries.containsKey(slotKey);
|
||||
}
|
||||
return !_isPlaying && _stackedOverlayEntries.isEmpty;
|
||||
}
|
||||
|
||||
void _playMessage(SCFloatingMessage message) {
|
||||
if (_isStackedFloatingMessage(message)) {
|
||||
_playStackedMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
_isPlaying = true;
|
||||
_currentMessage = message;
|
||||
final context = navigatorKey.currentState?.context;
|
||||
@ -296,21 +337,6 @@ class OverlayManager {
|
||||
return;
|
||||
}
|
||||
|
||||
_currentOverlayEntry = OverlayEntry(
|
||||
builder:
|
||||
(_) => Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 70.w),
|
||||
child: RepaintBoundary(child: _buildScreenWidget(message)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_currentOverlayEntry!);
|
||||
}
|
||||
|
||||
Widget _buildScreenWidget(SCFloatingMessage message) {
|
||||
bool completed = false;
|
||||
|
||||
void onComplete() {
|
||||
@ -330,6 +356,66 @@ class OverlayManager {
|
||||
}
|
||||
}
|
||||
|
||||
_currentOverlayEntry = OverlayEntry(
|
||||
builder:
|
||||
(_) => Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 70.w),
|
||||
child: RepaintBoundary(
|
||||
child: _buildScreenWidget(message, onComplete),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_currentOverlayEntry!);
|
||||
}
|
||||
|
||||
void _playStackedMessage(SCFloatingMessage message) {
|
||||
final slotKey = _stackSlotKeyFor(message);
|
||||
final context = navigatorKey.currentState?.context;
|
||||
if (slotKey == null || context == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
if (_stackedOverlayEntries.containsKey(slotKey)) {
|
||||
_messageQueue.add(message);
|
||||
return;
|
||||
}
|
||||
|
||||
bool completed = false;
|
||||
|
||||
void onComplete() {
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
|
||||
_removeStackedMessage(slotKey);
|
||||
_markStackedEntriesNeedsBuild();
|
||||
_safeScheduleNext();
|
||||
}
|
||||
|
||||
_stackedMessages[slotKey] = message;
|
||||
final entry = OverlayEntry(
|
||||
builder:
|
||||
(_) => Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _stackedWrapperTopFor(message).w),
|
||||
child: RepaintBoundary(
|
||||
child: _buildScreenWidget(message, onComplete),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
_stackedOverlayEntries[slotKey] = entry;
|
||||
Overlay.of(context).insert(entry);
|
||||
_markStackedEntriesNeedsBuild();
|
||||
}
|
||||
|
||||
Widget _buildScreenWidget(
|
||||
SCFloatingMessage message,
|
||||
VoidCallback onComplete,
|
||||
) {
|
||||
switch (message.type) {
|
||||
case 0:
|
||||
return FloatingLuckGiftScreenWidget(
|
||||
@ -372,6 +458,94 @@ class OverlayManager {
|
||||
}
|
||||
}
|
||||
|
||||
int? _stackSlotKeyFor(SCFloatingMessage message) {
|
||||
switch (message.type) {
|
||||
case 0:
|
||||
case 1:
|
||||
return 1;
|
||||
case 3:
|
||||
return 3;
|
||||
case 4:
|
||||
return 4;
|
||||
case 5:
|
||||
return 5;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isStackedFloatingMessage(SCFloatingMessage message) {
|
||||
return _stackSlotKeyFor(message) != null;
|
||||
}
|
||||
|
||||
double _stackedWrapperTopFor(SCFloatingMessage message) {
|
||||
final slotKey = _stackSlotKeyFor(message);
|
||||
if (slotKey == null) {
|
||||
return _stackedFloatingBaseTop;
|
||||
}
|
||||
var visualTop = _stackedFloatingBaseTop;
|
||||
for (final key in _stackedFloatingOrder) {
|
||||
if (key == slotKey) {
|
||||
break;
|
||||
}
|
||||
final previousMessage = _stackedMessages[key];
|
||||
if (previousMessage != null) {
|
||||
visualTop +=
|
||||
_stackedVisualHeightFor(previousMessage) + _stackedFloatingGap;
|
||||
}
|
||||
}
|
||||
final wrapperTop = visualTop - _stackedInternalTopFor(message);
|
||||
return wrapperTop < 0 ? 0 : wrapperTop;
|
||||
}
|
||||
|
||||
double _stackedVisualHeightFor(SCFloatingMessage message) {
|
||||
switch (message.type) {
|
||||
case 0:
|
||||
return 83;
|
||||
case 1:
|
||||
return 54;
|
||||
case 3:
|
||||
return 68;
|
||||
case 4:
|
||||
return 66;
|
||||
case 5:
|
||||
return 84;
|
||||
default:
|
||||
return 68;
|
||||
}
|
||||
}
|
||||
|
||||
double _stackedInternalTopFor(SCFloatingMessage message) {
|
||||
switch (message.type) {
|
||||
case 1:
|
||||
return 20;
|
||||
case 4:
|
||||
return 42;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void _markStackedEntriesNeedsBuild() {
|
||||
for (final entry in _stackedOverlayEntries.values) {
|
||||
entry.markNeedsBuild();
|
||||
}
|
||||
}
|
||||
|
||||
void _removeStackedMessage(int slotKey) {
|
||||
try {
|
||||
_stackedOverlayEntries.remove(slotKey)?.remove();
|
||||
} catch (_) {}
|
||||
_stackedMessages.remove(slotKey);
|
||||
}
|
||||
|
||||
void _removeAllStackedMessages() {
|
||||
final slotKeys = List<int>.from(_stackedOverlayEntries.keys);
|
||||
for (final slotKey in slotKeys) {
|
||||
_removeStackedMessage(slotKey);
|
||||
}
|
||||
}
|
||||
|
||||
void activate() {
|
||||
_isDisposed = false;
|
||||
_loadSavedDisplayPreference();
|
||||
@ -419,6 +593,7 @@ class OverlayManager {
|
||||
void beginSuppressFloatingScreens({String reason = 'unknown'}) {
|
||||
_suppressedCount += 1;
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
_removeAllStackedMessages();
|
||||
_messageQueue.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
@ -439,6 +614,7 @@ class OverlayManager {
|
||||
}
|
||||
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
_removeAllStackedMessages();
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
return;
|
||||
@ -450,8 +626,10 @@ class OverlayManager {
|
||||
context.mounted &&
|
||||
activeMessage != null &&
|
||||
!_shouldDisplayMessage(context, activeMessage)) {
|
||||
_removeActiveMessage();
|
||||
return;
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
}
|
||||
if (context != null && context.mounted) {
|
||||
_removeHiddenStackedMessages(context);
|
||||
}
|
||||
_safeScheduleNext();
|
||||
}
|
||||
@ -463,6 +641,7 @@ class OverlayManager {
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_removeAllStackedMessages();
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
_recentCompensationKeys.clear();
|
||||
@ -470,15 +649,26 @@ class OverlayManager {
|
||||
_isProcessing = false;
|
||||
}
|
||||
|
||||
void removeRoom() {
|
||||
_removeActiveRoomMessage();
|
||||
void removeRoom({String? roomId}) {
|
||||
final normalizedRoomId = roomId?.trim() ?? '';
|
||||
_removeActiveRoomMessage(
|
||||
roomId: normalizedRoomId,
|
||||
includeRedPacket: normalizedRoomId.isNotEmpty,
|
||||
);
|
||||
_removeMessagesByType(1);
|
||||
_removeMessagesByType(0);
|
||||
_removeMessagesByType(5);
|
||||
if (normalizedRoomId.isNotEmpty) {
|
||||
_removeMessagesByType(3, roomId: normalizedRoomId);
|
||||
}
|
||||
if (normalizedRoomId.isNotEmpty) {
|
||||
_removeMessagesByType(4, roomId: normalizedRoomId);
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法:移除特定类型的消息
|
||||
void _removeMessagesByType(int type) {
|
||||
void _removeMessagesByType(int type, {String? roomId}) {
|
||||
final normalizedRoomId = roomId?.trim() ?? '';
|
||||
// 由于 PriorityQueue 没有直接的方法来移除特定元素,
|
||||
// 我们需要重建队列,排除指定类型的消息
|
||||
final newQueue = SCPriorityQueue<SCFloatingMessage>(
|
||||
@ -488,7 +678,11 @@ class OverlayManager {
|
||||
// 遍历原始队列,只添加非指定类型的消息
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
final message = _messageQueue.removeFirst();
|
||||
if (message.type != type) {
|
||||
final matchesType = message.type == type;
|
||||
final matchesRoom =
|
||||
normalizedRoomId.isEmpty ||
|
||||
(message.roomId ?? "").trim() == normalizedRoomId;
|
||||
if (!matchesType || !matchesRoom) {
|
||||
newQueue.add(message);
|
||||
}
|
||||
}
|
||||
@ -497,12 +691,20 @@ class OverlayManager {
|
||||
while (newQueue.isNotEmpty) {
|
||||
_messageQueue.add(newQueue.removeFirst());
|
||||
}
|
||||
|
||||
_removeStackedMessagesWhere((message) {
|
||||
final matchesType = message.type == type;
|
||||
final matchesRoom =
|
||||
normalizedRoomId.isEmpty ||
|
||||
(message.roomId ?? "").trim() == normalizedRoomId;
|
||||
return matchesType && matchesRoom;
|
||||
});
|
||||
}
|
||||
|
||||
// 辅助方法:获取队列信息
|
||||
int get queueLength => _messageQueue.length;
|
||||
|
||||
bool get isPlaying => _isPlaying;
|
||||
bool get isPlaying => _isPlaying || _stackedOverlayEntries.isNotEmpty;
|
||||
|
||||
bool get _isSuppressed => _suppressedCount > 0;
|
||||
|
||||
@ -561,19 +763,6 @@ class OverlayManager {
|
||||
}
|
||||
}
|
||||
|
||||
String _displayStateForLog(BuildContext context) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||
return 'homeVisible=$_homeRootTabsVisible '
|
||||
'roomVisible=${rtcProvider.shouldShowRoomVisualEffects} '
|
||||
'currentRoomId=$currentRoomId '
|
||||
'scope=${SCGlobalConfig.floatingBroadcastScope}';
|
||||
}
|
||||
|
||||
bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) {
|
||||
if (message == null) {
|
||||
return false;
|
||||
@ -605,7 +794,8 @@ class OverlayManager {
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
if (!rtcProvider.shouldShowRoomVisualEffects) {
|
||||
if (!rtcProvider.shouldShowRoomVisualEffects ||
|
||||
!rtcProvider.isVoiceRoomRouteVisible) {
|
||||
return false;
|
||||
}
|
||||
final currentRoomId =
|
||||
@ -633,6 +823,7 @@ class OverlayManager {
|
||||
final messageRoomId = (message.roomId ?? "").trim();
|
||||
final isCurrentVisibleRoom =
|
||||
rtcProvider.shouldShowRoomVisualEffects &&
|
||||
rtcProvider.isVoiceRoomRouteVisible &&
|
||||
currentRoomId.isNotEmpty &&
|
||||
messageRoomId.isNotEmpty &&
|
||||
currentRoomId == messageRoomId;
|
||||
@ -640,6 +831,13 @@ class OverlayManager {
|
||||
// 区域红包在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。
|
||||
return message.type == 4;
|
||||
}
|
||||
final isCurrentRoomMessage =
|
||||
currentRoomId.isNotEmpty &&
|
||||
messageRoomId.isNotEmpty &&
|
||||
currentRoomId == messageRoomId;
|
||||
if (message.type == 4 && isCurrentRoomMessage) {
|
||||
return false;
|
||||
}
|
||||
if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) {
|
||||
return true;
|
||||
}
|
||||
@ -674,25 +872,37 @@ class OverlayManager {
|
||||
|
||||
void _removeActiveRegionBroadcastMessage() {
|
||||
final activeMessage = _currentMessage;
|
||||
if (activeMessage == null || !activeMessage.isRegionBroadcast) {
|
||||
return;
|
||||
if (activeMessage != null && activeMessage.isRegionBroadcast) {
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_isPlaying = false;
|
||||
}
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_isPlaying = false;
|
||||
_removeStackedMessagesWhere((message) => message.isRegionBroadcast);
|
||||
_safeScheduleNext();
|
||||
}
|
||||
|
||||
void _removeActiveRoomMessage() {
|
||||
void _removeActiveRoomMessage({
|
||||
String? roomId,
|
||||
bool includeRedPacket = false,
|
||||
}) {
|
||||
final activeMessage = _currentMessage;
|
||||
if (activeMessage == null ||
|
||||
(activeMessage.type != 0 &&
|
||||
activeMessage.type != 1 &&
|
||||
activeMessage.type != 5)) {
|
||||
return;
|
||||
final normalizedRoomId = roomId?.trim() ?? '';
|
||||
if (activeMessage != null &&
|
||||
(activeMessage.type == 0 ||
|
||||
activeMessage.type == 1 ||
|
||||
activeMessage.type == 3 ||
|
||||
activeMessage.type == 5 ||
|
||||
(includeRedPacket && activeMessage.type == 4)) &&
|
||||
(normalizedRoomId.isEmpty ||
|
||||
(activeMessage.roomId ?? "").trim() == normalizedRoomId)) {
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
}
|
||||
_removeActiveMessage();
|
||||
_removeStackedRoomMessages(
|
||||
roomId: normalizedRoomId,
|
||||
includeRedPacket: includeRedPacket,
|
||||
);
|
||||
_safeScheduleNext();
|
||||
}
|
||||
|
||||
void _removeActiveMessage({bool scheduleNext = true}) {
|
||||
@ -704,4 +914,47 @@ class OverlayManager {
|
||||
_safeScheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
void _removeHiddenStackedMessages(BuildContext context) {
|
||||
_removeStackedMessagesWhere(
|
||||
(message) => !_shouldDisplayMessage(context, message),
|
||||
);
|
||||
}
|
||||
|
||||
void _removeStackedRoomMessages({
|
||||
required String roomId,
|
||||
required bool includeRedPacket,
|
||||
}) {
|
||||
_removeStackedMessagesWhere((message) {
|
||||
final type = message.type;
|
||||
final isRoomFloatingType =
|
||||
type == 0 ||
|
||||
type == 1 ||
|
||||
type == 3 ||
|
||||
type == 5 ||
|
||||
(includeRedPacket && type == 4);
|
||||
if (!isRoomFloatingType) {
|
||||
return false;
|
||||
}
|
||||
return roomId.isEmpty || (message.roomId ?? "").trim() == roomId;
|
||||
});
|
||||
}
|
||||
|
||||
void _removeStackedMessagesWhere(
|
||||
bool Function(SCFloatingMessage message) test,
|
||||
) {
|
||||
final matchedSlotKeys = <int>[];
|
||||
_stackedMessages.forEach((slotKey, message) {
|
||||
if (test(message)) {
|
||||
matchedSlotKeys.add(slotKey);
|
||||
}
|
||||
});
|
||||
if (matchedSlotKeys.isEmpty) {
|
||||
return;
|
||||
}
|
||||
for (final slotKey in matchedSlotKeys) {
|
||||
_removeStackedMessage(slotKey);
|
||||
}
|
||||
_markStackedEntriesNeedsBuild();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||||
@ -296,7 +298,10 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
Future<SCVersionManageLatestRes> versionManageLatest() async {
|
||||
final result = await http.get<SCVersionManageLatestRes>(
|
||||
"ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687",
|
||||
fromJson: (json) => SCVersionManageLatestRes.fromJson(json),
|
||||
fromJson: (json) {
|
||||
_debugVersionResponse('releaseLatest', json);
|
||||
return SCVersionManageLatestRes.fromJson(json);
|
||||
},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
@ -306,11 +311,22 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
Future<VersionManageLatesReviewRes> versionManageLatestReview() async {
|
||||
final result = await http.get<VersionManageLatesReviewRes>(
|
||||
"ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232",
|
||||
fromJson: (json) => VersionManageLatesReviewRes.fromJson(json),
|
||||
fromJson: (json) {
|
||||
_debugVersionResponse('latestReview', json);
|
||||
return VersionManageLatesReviewRes.fromJson(json);
|
||||
},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
void _debugVersionResponse(String name, dynamic json) {
|
||||
try {
|
||||
debugPrint('[SCVersionUtils] $name rawBody=${jsonEncode(json)}');
|
||||
} catch (e) {
|
||||
debugPrint('[SCVersionUtils] $name rawBody=$json encodeError=$e');
|
||||
}
|
||||
}
|
||||
|
||||
///sys/config/customer-service
|
||||
@override
|
||||
Future<SocialChatUserProfile> customerService() async {
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:yumi/shared/business_logic/models/res/room_res.dart'
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_grab_res.dart';
|
||||
@ -183,6 +184,7 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
String? pwd,
|
||||
String redPackId = "",
|
||||
bool needOpenRedenvelope = false,
|
||||
bool silentErrorToast = false,
|
||||
}) async {
|
||||
var param = {};
|
||||
param["roomId"] = roomId;
|
||||
@ -195,6 +197,10 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
final result = await http.post(
|
||||
"363603111e51beac2d183014dd29b81c219577507bcc9c6abfaf327548fed0e0",
|
||||
data: param,
|
||||
extra:
|
||||
silentErrorToast
|
||||
? const {BaseNetworkClient.silentErrorToastKey: true}
|
||||
: null,
|
||||
fromJson: (json) => JoinRoomRes.fromJson(json),
|
||||
);
|
||||
return SCRoomProfileCache.applyToJoinRoomRes(result);
|
||||
@ -607,7 +613,20 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
final result = await http.get<SCUserIdentityRes>(
|
||||
"d29121f09fa26c45b5322df6de0fc25b",
|
||||
queryParams: parm,
|
||||
fromJson: (json) => SCUserIdentityRes.fromJson(json),
|
||||
fromJson: (json) {
|
||||
final identity = SCUserIdentityRes.fromJson(json);
|
||||
if (kDebugMode) {
|
||||
debugPrint('[SCUserIdentity] raw=${jsonEncode(json)}');
|
||||
debugPrint(
|
||||
'[SCUserIdentity] parsed admin=${identity.admin}, '
|
||||
'manager=${identity.manager}, yumiManager=${identity.yumiManager}, '
|
||||
'raw_manager=${json['manager']}(${json['manager']?.runtimeType}), '
|
||||
'raw_yumi_manager=${json['yumi_manager']}(${json['yumi_manager']?.runtimeType}), '
|
||||
'raw_yumiManager=${json['yumiManager']}(${json['yumiManager']?.runtimeType})',
|
||||
);
|
||||
}
|
||||
return identity;
|
||||
},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
@ -1168,6 +1187,32 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
///app/user/badge/current
|
||||
@override
|
||||
Future<SCUserBadgeRes> currentUserBadges() async {
|
||||
const path = "/app/user/badge/current";
|
||||
final result = await http.get<SCUserBadgeRes>(
|
||||
path,
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCUserBadgeRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///app/user/badge/other
|
||||
@override
|
||||
Future<SCUserBadgeRes> otherUserBadges(String userId) async {
|
||||
final params = <String, dynamic>{"userId": userId};
|
||||
const path = "/app/user/badge/other";
|
||||
final result = await http.get<SCUserBadgeRes>(
|
||||
path,
|
||||
queryParams: params,
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) => SCUserBadgeRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///user/badge/toggle
|
||||
@override
|
||||
Future<bool> badgeToggle(String badgeId) async {
|
||||
|
||||
@ -23,8 +23,7 @@ class SCGiftVapSvgaManager {
|
||||
static const int _maxConsecutiveEntryTasksBeforeGift = 2;
|
||||
static const Duration _taskWatchdogTimeout = Duration(seconds: 20);
|
||||
static const Duration _entryTaskTtl = Duration(seconds: 6);
|
||||
final Map<String, MovieEntity> _videoItemCache = <String, MovieEntity>{};
|
||||
final Map<MovieEntity, int> _svgaEntityUseCounts = <MovieEntity, int>{};
|
||||
Map<String, MovieEntity> videoItemCache = {};
|
||||
static SCGiftVapSvgaManager? _inst;
|
||||
static const int _maxPreloadConcurrency = 1;
|
||||
static const int _maxPreloadQueueLength = 12;
|
||||
@ -80,44 +79,6 @@ class SCGiftVapSvgaManager {
|
||||
return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga";
|
||||
}
|
||||
|
||||
Future<MovieEntity> acquireSvgaEntity(String path) async {
|
||||
final entity = await _loadSvgaEntity(path);
|
||||
_retainSvgaEntity(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
void releaseSvgaEntity(MovieEntity? entity) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
final count = _svgaEntityUseCounts[entity] ?? 0;
|
||||
if (count <= 1) {
|
||||
_svgaEntityUseCounts.remove(entity);
|
||||
if (!_videoItemCache.containsValue(entity) &&
|
||||
!_isSvgaEntityInUse(entity)) {
|
||||
_disposeSvgaEntity(entity);
|
||||
} else {
|
||||
_trimSvgaCache();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_svgaEntityUseCounts[entity] = count - 1;
|
||||
}
|
||||
|
||||
void evictSvgaEntity(String path, {bool includeCurrent = false}) {
|
||||
final entity = _videoItemCache.remove(path);
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
if (includeCurrent || !_isSvgaEntityInUse(entity)) {
|
||||
_disposeSvgaEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
void _retainSvgaEntity(MovieEntity entity) {
|
||||
_svgaEntityUseCounts[entity] = (_svgaEntityUseCounts[entity] ?? 0) + 1;
|
||||
}
|
||||
|
||||
static int resolveEffectPriority({
|
||||
int priority = 0,
|
||||
int type = giftEffectType,
|
||||
@ -155,7 +116,7 @@ class SCGiftVapSvgaManager {
|
||||
|
||||
bool _isPreloadedOrLoading(String path) {
|
||||
if (_needsSvgaController(path)) {
|
||||
return _videoItemCache.containsKey(path) ||
|
||||
return videoItemCache.containsKey(path) ||
|
||||
_svgaLoadTasks.containsKey(path);
|
||||
}
|
||||
final pathType = SCPathUtils.getPathType(path);
|
||||
@ -295,19 +256,19 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
|
||||
MovieEntity? _touchCachedSvgaEntity(String path) {
|
||||
final cached = _videoItemCache.remove(path);
|
||||
final cached = videoItemCache.remove(path);
|
||||
if (cached != null) {
|
||||
_videoItemCache[path] = cached;
|
||||
videoItemCache[path] = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
void _cacheSvgaEntity(String path, MovieEntity entity) {
|
||||
final previous = _videoItemCache.remove(path);
|
||||
final previous = videoItemCache.remove(path);
|
||||
if (previous != null && !identical(previous, entity)) {
|
||||
_disposeSvgaEntityIfUnused(previous);
|
||||
}
|
||||
_videoItemCache[path] = entity;
|
||||
videoItemCache[path] = entity;
|
||||
_trimSvgaCache();
|
||||
}
|
||||
|
||||
@ -326,17 +287,17 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
|
||||
void _trimSvgaCache() {
|
||||
while (_videoItemCache.length > _maxSvgaCacheEntries) {
|
||||
final removableEntry = _videoItemCache.entries.firstWhere(
|
||||
while (videoItemCache.length > _maxSvgaCacheEntries) {
|
||||
final removableEntry = videoItemCache.entries.firstWhere(
|
||||
(entry) => !_isSvgaEntityInUse(entry.value),
|
||||
orElse: () => _videoItemCache.entries.first,
|
||||
orElse: () => videoItemCache.entries.first,
|
||||
);
|
||||
if (_isSvgaEntityInUse(removableEntry.value)) {
|
||||
_videoItemCache.remove(removableEntry.key);
|
||||
_videoItemCache[removableEntry.key] = removableEntry.value;
|
||||
videoItemCache.remove(removableEntry.key);
|
||||
videoItemCache[removableEntry.key] = removableEntry.value;
|
||||
break;
|
||||
}
|
||||
_videoItemCache.remove(removableEntry.key);
|
||||
videoItemCache.remove(removableEntry.key);
|
||||
_disposeSvgaEntity(removableEntry.value);
|
||||
}
|
||||
}
|
||||
@ -791,21 +752,20 @@ class SCGiftVapSvgaManager {
|
||||
_preloadQueue.clear();
|
||||
_queuedPreloadPaths.clear();
|
||||
_activePreloadCount = 0;
|
||||
final entries = _videoItemCache.entries.toList(growable: false);
|
||||
final entries = videoItemCache.entries.toList(growable: false);
|
||||
for (final entry in entries) {
|
||||
final entity = entry.value;
|
||||
if (!includeCurrent && _isSvgaEntityInUse(entity)) {
|
||||
continue;
|
||||
}
|
||||
_videoItemCache.remove(entry.key);
|
||||
videoItemCache.remove(entry.key);
|
||||
_disposeSvgaEntity(entity);
|
||||
}
|
||||
_playablePathCache.clear();
|
||||
}
|
||||
|
||||
bool _isSvgaEntityInUse(MovieEntity entity) {
|
||||
return identical(_rsc?.videoItem, entity) ||
|
||||
(_svgaEntityUseCounts[entity] ?? 0) > 0;
|
||||
return identical(_rsc?.videoItem, entity);
|
||||
}
|
||||
|
||||
void _disposeSvgaEntityIfUnused(MovieEntity entity) {
|
||||
@ -817,7 +777,6 @@ class SCGiftVapSvgaManager {
|
||||
|
||||
void _disposeSvgaEntity(MovieEntity entity) {
|
||||
try {
|
||||
_svgaEntityUseCounts.remove(entity);
|
||||
entity.dispose();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@ -53,6 +53,7 @@ class SCPathUtils {
|
||||
'.mp4',
|
||||
'.vap',
|
||||
'.svga',
|
||||
'.pag',
|
||||
].contains(ext);
|
||||
|
||||
return isDirectAsset && isSupportedAsset;
|
||||
@ -76,6 +77,7 @@ class SCPathUtils {
|
||||
'.mov',
|
||||
'.avi',
|
||||
'.svga',
|
||||
'.pag',
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.txt',
|
||||
|
||||
@ -23,6 +23,24 @@ typedef SCRoomUtils = SCChatRoomHelper;
|
||||
class SCChatRoomHelper {
|
||||
static Map<String, SocialChatUserProfile> roomUsersMap = {};
|
||||
|
||||
static bool isCurrentRoom(String roomId, BuildContext context) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final currentRoomId =
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
|
||||
'';
|
||||
return currentRoomId.isNotEmpty && currentRoomId == normalizedRoomId;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void goRoom(
|
||||
String roomId,
|
||||
BuildContext context, {
|
||||
|
||||
@ -1,14 +1,599 @@
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
|
||||
class SCVersionUtils {
|
||||
static Future<void> checkReview() async {
|
||||
try {
|
||||
var versionInfo = await SCConfigRepositoryImp().versionManageLatestReview();
|
||||
if (versionInfo.review != null) {
|
||||
SCGlobalConfig.isReview =
|
||||
"${versionInfo.review?.version}" == SCGlobalConfig.version;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_version_manage_latest_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_url_launcher_utils.dart';
|
||||
|
||||
class SCVersionUtils {
|
||||
static bool _forceUpdateDialogShowing = false;
|
||||
|
||||
static Future<bool> checkReview({BuildContext? context}) async {
|
||||
_debugLog(
|
||||
'checkReview start '
|
||||
'currentVersion=${SCGlobalConfig.version} '
|
||||
'currentBuild=${SCGlobalConfig.build} '
|
||||
'platform=${Platform.isIOS ? "ios" : "android"} '
|
||||
'apiHost=${SCGlobalConfig.apiHost} '
|
||||
'hasContext=${context != null} '
|
||||
'contextMounted=${context?.mounted}',
|
||||
);
|
||||
try {
|
||||
var versionInfo =
|
||||
await SCConfigRepositoryImp().versionManageLatestReview();
|
||||
_debugLog(
|
||||
'checkReview response '
|
||||
'review=${_versionInfoDebug(versionInfo.review)} '
|
||||
'latest=${_versionInfoDebug(versionInfo.latest)}',
|
||||
);
|
||||
if (versionInfo.review != null) {
|
||||
SCGlobalConfig.isReview =
|
||||
"${versionInfo.review?.version}" == SCGlobalConfig.version;
|
||||
_debugLog('checkReview isReview=${SCGlobalConfig.isReview}');
|
||||
}
|
||||
final latest = await _resolveLatestVersion(versionInfo.latest);
|
||||
final shouldForceUpdate = latest != null && _shouldForceUpdate(latest);
|
||||
_debugLog(
|
||||
'checkReview forceDecision '
|
||||
'shouldForceUpdate=$shouldForceUpdate '
|
||||
'reason=${_forceUpdateDecisionDebug(latest)}',
|
||||
);
|
||||
if (context != null &&
|
||||
context.mounted &&
|
||||
latest != null &&
|
||||
shouldForceUpdate) {
|
||||
SCLoadingManager.hide();
|
||||
_debugLog('checkReview showForceUpdateDialog');
|
||||
_showForceUpdateDialog(context, latest);
|
||||
return false;
|
||||
}
|
||||
if (shouldForceUpdate && context == null) {
|
||||
_debugLog('checkReview skip dialog reason=no_context');
|
||||
} else if (shouldForceUpdate &&
|
||||
context != null &&
|
||||
context.mounted == false) {
|
||||
_debugLog('checkReview skip dialog reason=context_not_mounted');
|
||||
} else {
|
||||
_debugLog('checkReview no force dialog');
|
||||
}
|
||||
} catch (e) {
|
||||
_debugLog('checkReview request failed error=$e');
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _shouldForceUpdate(SCVersionManageLatestRes latest) {
|
||||
if (latest.forceUpdate != true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final latestBuild = latest.buildVersion?.toInt() ?? 0;
|
||||
final currentBuild = _currentComparableBuild();
|
||||
final hasLatestVersion = (latest.version ?? '').trim().isNotEmpty;
|
||||
final versionCompare = _compareVersion(
|
||||
latest.version,
|
||||
SCGlobalConfig.version,
|
||||
);
|
||||
if (hasLatestVersion) {
|
||||
if (versionCompare > 0) {
|
||||
return true;
|
||||
}
|
||||
if (versionCompare < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (latestBuild > 0 && currentBuild > 0) {
|
||||
return latestBuild > currentBuild;
|
||||
}
|
||||
|
||||
return hasLatestVersion && versionCompare > 0;
|
||||
}
|
||||
|
||||
static Future<SCVersionManageLatestRes?> _resolveLatestVersion(
|
||||
SCVersionManageLatestRes? latest,
|
||||
) async {
|
||||
if (!_isVersionInfoEmpty(latest)) {
|
||||
return latest;
|
||||
}
|
||||
_debugLog('checkReview latestReview latest empty, fallback releaseLatest');
|
||||
try {
|
||||
final fallback = await SCConfigRepositoryImp().versionManageLatest();
|
||||
_debugLog('checkReview fallbackLatest=${_versionInfoDebug(fallback)}');
|
||||
return _isVersionInfoEmpty(fallback) ? latest : fallback;
|
||||
} catch (e) {
|
||||
_debugLog('checkReview fallback releaseLatest failed error=$e');
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isVersionInfoEmpty(SCVersionManageLatestRes? version) {
|
||||
if (version == null) {
|
||||
return true;
|
||||
}
|
||||
return (version.id ?? '').trim().isEmpty &&
|
||||
(version.platform ?? '').trim().isEmpty &&
|
||||
(version.version ?? '').trim().isEmpty &&
|
||||
version.forceUpdate == null &&
|
||||
(version.appType ?? '').trim().isEmpty &&
|
||||
version.review == null &&
|
||||
version.patch == null &&
|
||||
(version.updateDescribe ?? '').trim().isEmpty &&
|
||||
(version.updateWorshipDescribe ?? '').trim().isEmpty &&
|
||||
(version.downloadUrl ?? '').trim().isEmpty &&
|
||||
version.buildVersion == null &&
|
||||
version.createTime == null;
|
||||
}
|
||||
|
||||
static int _compareVersion(String? latestVersion, String currentVersion) {
|
||||
final latestParts = _versionParts(latestVersion);
|
||||
final currentParts = _versionParts(currentVersion);
|
||||
final maxLength =
|
||||
latestParts.length > currentParts.length
|
||||
? latestParts.length
|
||||
: currentParts.length;
|
||||
for (var i = 0; i < maxLength; i++) {
|
||||
final latestPart = i < latestParts.length ? latestParts[i] : 0;
|
||||
final currentPart = i < currentParts.length ? currentParts[i] : 0;
|
||||
if (latestPart != currentPart) {
|
||||
return latestPart.compareTo(currentPart);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static List<int> _versionParts(String? version) {
|
||||
final normalized = version?.trim() ?? "";
|
||||
if (normalized.isEmpty) {
|
||||
return const <int>[];
|
||||
}
|
||||
return normalized
|
||||
.split(RegExp(r'[.+\-_]'))
|
||||
.map(
|
||||
(part) => int.tryParse(RegExp(r'^\d+').stringMatch(part) ?? '0') ?? 0,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static int _currentRawBuild() {
|
||||
return int.tryParse(SCGlobalConfig.build.trim()) ?? 0;
|
||||
}
|
||||
|
||||
static int _currentComparableBuild() {
|
||||
final rawBuild = _currentRawBuild();
|
||||
if (!Platform.isAndroid || rawBuild < 1000) {
|
||||
return rawBuild;
|
||||
}
|
||||
|
||||
// Flutter split-per-abi APKs offset Android versionCode by ABI, e.g.
|
||||
// build 132 becomes 2132 for arm64-v8a. The backend stores the base build.
|
||||
final baseBuild = rawBuild % 1000;
|
||||
return baseBuild > 0 ? baseBuild : rawBuild;
|
||||
}
|
||||
|
||||
static String _currentBuildDebug() {
|
||||
final rawBuild = _currentRawBuild();
|
||||
final comparableBuild = _currentComparableBuild();
|
||||
if (rawBuild == comparableBuild) {
|
||||
return '$rawBuild';
|
||||
}
|
||||
return '$comparableBuild(raw=$rawBuild)';
|
||||
}
|
||||
|
||||
static String _forceUpdateDecisionDebug(SCVersionManageLatestRes? latest) {
|
||||
if (latest == null) {
|
||||
return 'latest_null';
|
||||
}
|
||||
final latestBuild = latest.buildVersion?.toInt() ?? 0;
|
||||
final currentBuild = _currentComparableBuild();
|
||||
final currentBuildDebug = _currentBuildDebug();
|
||||
final versionCompare = _compareVersion(
|
||||
latest.version,
|
||||
SCGlobalConfig.version,
|
||||
);
|
||||
if (latest.forceUpdate != true) {
|
||||
return 'forceUpdate_not_true '
|
||||
'forceUpdate=${latest.forceUpdate} '
|
||||
'latestBuild=$latestBuild currentBuild=$currentBuildDebug '
|
||||
'versionCompare=$versionCompare';
|
||||
}
|
||||
if (latestBuild > 0 && currentBuild > 0) {
|
||||
return 'compare_build '
|
||||
'latestBuild=$latestBuild currentBuild=$currentBuildDebug '
|
||||
'versionCompare=$versionCompare';
|
||||
}
|
||||
return 'compare_version '
|
||||
'latestVersion=${latest.version} '
|
||||
'currentVersion=${SCGlobalConfig.version} '
|
||||
'versionCompare=$versionCompare '
|
||||
'latestBuild=$latestBuild currentBuild=$currentBuildDebug';
|
||||
}
|
||||
|
||||
static String _versionInfoDebug(SCVersionManageLatestRes? version) {
|
||||
if (version == null) {
|
||||
return 'null';
|
||||
}
|
||||
return '{'
|
||||
'id=${version.id}, '
|
||||
'platform=${version.platform}, '
|
||||
'version=${version.version}, '
|
||||
'buildVersion=${version.buildVersion}, '
|
||||
'forceUpdate=${version.forceUpdate}, '
|
||||
'review=${version.review}, '
|
||||
'patch=${version.patch}, '
|
||||
'downloadUrl=${version.downloadUrl}'
|
||||
'}';
|
||||
}
|
||||
|
||||
static void _debugLog(String message) {
|
||||
debugPrint('[SCVersionUtils] $message');
|
||||
}
|
||||
|
||||
static void _showForceUpdateDialog(
|
||||
BuildContext context,
|
||||
SCVersionManageLatestRes latest,
|
||||
) {
|
||||
if (_forceUpdateDialogShowing || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
_forceUpdateDialogShowing = true;
|
||||
unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.black.withValues(alpha: 0.62),
|
||||
builder: (dialogContext) {
|
||||
final version = (latest.version ?? "").trim();
|
||||
final updateDescribe = (latest.updateDescribe ?? "").trim();
|
||||
final message =
|
||||
updateDescribe.isNotEmpty
|
||||
? updateDescribe
|
||||
: "A newer version is required to keep voice rooms stable "
|
||||
"and secure.";
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: Dialog(
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 28),
|
||||
child: SCForceUpdateDialog(
|
||||
version: version,
|
||||
message: message,
|
||||
onUpdate: () {
|
||||
unawaited(_openUpdateUrl(latest));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).whenComplete(() {
|
||||
_forceUpdateDialogShowing = false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _openUpdateUrl(SCVersionManageLatestRes latest) async {
|
||||
final url = _resolveDownloadUrl(latest);
|
||||
if (url.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await SCUrlLauncherUtils.launchExternal(url);
|
||||
}
|
||||
|
||||
static String _resolveDownloadUrl(SCVersionManageLatestRes latest) {
|
||||
final downloadUrl = (latest.downloadUrl ?? "").trim();
|
||||
if (downloadUrl.isNotEmpty) {
|
||||
return downloadUrl;
|
||||
}
|
||||
return Platform.isIOS
|
||||
? SCGlobalConfig.appDownloadUrlApple
|
||||
: SCGlobalConfig.appDownloadUrlGoogle;
|
||||
}
|
||||
}
|
||||
|
||||
class SCForceUpdateDialog extends StatelessWidget {
|
||||
const SCForceUpdateDialog({
|
||||
super.key,
|
||||
required this.version,
|
||||
required this.message,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
final String version;
|
||||
final String message;
|
||||
final VoidCallback onUpdate;
|
||||
|
||||
static const _asset = 'sc_images/general/sc_icon_app_update_bg.png';
|
||||
static const _primary = Color(0xff18F2B1);
|
||||
static const _primaryDark = Color(0xff10C894);
|
||||
static const _titleColor = Color(0xff151823);
|
||||
static const _bodyColor = Color(0xff626775);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final constrainedTextScaler = MediaQuery.textScalerOf(
|
||||
context,
|
||||
).clamp(maxScaleFactor: 1.15);
|
||||
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaler: constrainedTextScaler),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width =
|
||||
constraints.maxWidth.isFinite
|
||||
? constraints.maxWidth.clamp(280.0, 340.0)
|
||||
: 340.0;
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(width: width),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
blurRadius: 34,
|
||||
offset: const Offset(0, 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 164,
|
||||
width: double.infinity,
|
||||
child: Image.asset(
|
||||
_asset,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
frameBuilder: (
|
||||
context,
|
||||
child,
|
||||
frame,
|
||||
wasSynchronouslyLoaded,
|
||||
) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return const _UpdateHeroFallback();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const _UpdateHeroFallback();
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_VersionPill(version: version),
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
'Update Required',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: _titleColor,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
message,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: _bodyColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const _UpdateNotice(),
|
||||
const SizedBox(height: 22),
|
||||
_UpdateButton(onPressed: onUpdate),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateHeroFallback extends StatelessWidget {
|
||||
const _UpdateHeroFallback();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xff14D6A1), Color(0xff65F3CD)],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 34,
|
||||
top: 26,
|
||||
child: Icon(
|
||||
Icons.auto_awesome_rounded,
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 44,
|
||||
top: 36,
|
||||
child: Icon(
|
||||
Icons.auto_awesome_rounded,
|
||||
color: Colors.white.withValues(alpha: 0.58),
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 86,
|
||||
height: 86,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.92),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.12),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.rocket_launch_rounded,
|
||||
color: SCForceUpdateDialog._primaryDark,
|
||||
size: 46,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VersionPill extends StatelessWidget {
|
||||
const _VersionPill({required this.version});
|
||||
|
||||
final String version;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label =
|
||||
version.isEmpty ? 'New version available' : 'Version $version';
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: SCForceUpdateDialog._primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(
|
||||
color: SCForceUpdateDialog._primary.withValues(alpha: 0.28),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: SCForceUpdateDialog._primaryDark,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateNotice extends StatelessWidget {
|
||||
const _UpdateNotice();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF6F8FA),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: const Text(
|
||||
'This Agora-based version has expired. Please update to the latest '
|
||||
'version to continue using voice rooms normally.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: SCForceUpdateDialog._titleColor,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.36,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateButton extends StatelessWidget {
|
||||
const _UpdateButton({required this.onPressed});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Ink(
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [SCForceUpdateDialog._primary, Color(0xff76FFD8)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: SCForceUpdateDialog._primary.withValues(alpha: 0.34),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: const Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Update Now',
|
||||
style: TextStyle(
|
||||
color: Color(0xff10131A),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_forward_rounded,
|
||||
color: Color(0xff10131A),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
183
lib/ui_kit/widgets/badge/sc_user_badge_strip.dart
Normal file
@ -0,0 +1,183 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
|
||||
class SCUserBadgeStrip extends StatefulWidget {
|
||||
const SCUserBadgeStrip({
|
||||
super.key,
|
||||
this.userId,
|
||||
this.fallbackBadges = const <WearBadge>[],
|
||||
this.height,
|
||||
this.badgeHeight,
|
||||
this.longBadgeWidth,
|
||||
this.spacing,
|
||||
this.maxBadges,
|
||||
this.wrap = false,
|
||||
this.reserveSpace = false,
|
||||
this.alignment = WrapAlignment.center,
|
||||
});
|
||||
|
||||
final String? userId;
|
||||
final List<WearBadge> fallbackBadges;
|
||||
final double? height;
|
||||
final double? badgeHeight;
|
||||
final double? longBadgeWidth;
|
||||
final double? spacing;
|
||||
final int? maxBadges;
|
||||
final bool wrap;
|
||||
final bool reserveSpace;
|
||||
final WrapAlignment alignment;
|
||||
|
||||
@override
|
||||
State<SCUserBadgeStrip> createState() => _SCUserBadgeStripState();
|
||||
}
|
||||
|
||||
class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
|
||||
static final Map<String, Future<SCUserBadgeRes>> _badgeFutures = {};
|
||||
|
||||
Future<SCUserBadgeRes>? _future;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_future = _resolveFuture();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SCUserBadgeStrip oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.userId != widget.userId) {
|
||||
_future = _resolveFuture();
|
||||
}
|
||||
}
|
||||
|
||||
Future<SCUserBadgeRes>? _resolveFuture() {
|
||||
final userId = _normalizedUserId;
|
||||
if (userId.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final currentUserId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
|
||||
final isCurrentUser = currentUserId.isNotEmpty && userId == currentUserId;
|
||||
final cacheKey = isCurrentUser ? "current:$userId" : "other:$userId";
|
||||
return _badgeFutures.putIfAbsent(cacheKey, () async {
|
||||
try {
|
||||
return isCurrentUser
|
||||
? await SCAccountRepository().currentUserBadges()
|
||||
: await SCAccountRepository().otherUserBadges(userId);
|
||||
} catch (_) {
|
||||
_badgeFutures.remove(cacheKey);
|
||||
return SCUserBadgeRes.empty(userId: userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String get _normalizedUserId => widget.userId?.trim() ?? "";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final stripHeight = widget.height ?? 25.w;
|
||||
final future = _future;
|
||||
if (future == null) {
|
||||
return _buildBadges(_fallbackBadges, stripHeight);
|
||||
}
|
||||
return FutureBuilder<SCUserBadgeRes>(
|
||||
future: future,
|
||||
builder: (context, snapshot) {
|
||||
final remoteBadges =
|
||||
snapshot.data?.displayBadges ?? const <WearBadge>[];
|
||||
final badges = remoteBadges.isNotEmpty ? remoteBadges : _fallbackBadges;
|
||||
return _buildBadges(badges, stripHeight);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<WearBadge> get _fallbackBadges {
|
||||
return widget.fallbackBadges
|
||||
.where((badge) => _badgeUrl(badge).isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Widget _buildBadges(List<WearBadge> rawBadges, double stripHeight) {
|
||||
final badges =
|
||||
widget.maxBadges == null
|
||||
? rawBadges
|
||||
: rawBadges.take(widget.maxBadges!).toList();
|
||||
if (badges.isEmpty) {
|
||||
return widget.reserveSpace
|
||||
? SizedBox(height: stripHeight)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final badgeHeight = widget.badgeHeight ?? stripHeight;
|
||||
final spacing = widget.spacing ?? 5.w;
|
||||
final children =
|
||||
badges
|
||||
.map((badge) => _buildBadge(badge, badgeHeight: badgeHeight))
|
||||
.toList();
|
||||
|
||||
if (widget.wrap) {
|
||||
return SizedBox(
|
||||
height: stripHeight,
|
||||
child: Center(
|
||||
child: Wrap(
|
||||
alignment: widget.alignment,
|
||||
spacing: spacing,
|
||||
runSpacing: 5.w,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: stripHeight,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Row(children: _withSpacing(children, spacing)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBadge(WearBadge badge, {required double badgeHeight}) {
|
||||
final type = badge.type?.trim().toUpperCase() ?? "";
|
||||
final isLongBadge = type == "LONG";
|
||||
final width =
|
||||
isLongBadge
|
||||
? (widget.longBadgeWidth ?? badgeHeight * 2.2)
|
||||
: badgeHeight;
|
||||
return netImage(
|
||||
url: _badgeUrl(badge),
|
||||
width: width,
|
||||
height: badgeHeight,
|
||||
fit: BoxFit.contain,
|
||||
noDefaultImg: true,
|
||||
loadingWidget: const SizedBox.shrink(),
|
||||
errorWidget: const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
String _badgeUrl(WearBadge badge) {
|
||||
return (badge.selectUrl ?? badge.notSelectUrl ?? badge.animationUrl ?? "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
List<Widget> _withSpacing(List<Widget> children, double spacing) {
|
||||
if (children.length < 2) {
|
||||
return children;
|
||||
}
|
||||
final spaced = <Widget>[];
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
if (i > 0) {
|
||||
spaced.add(SizedBox(width: spacing));
|
||||
}
|
||||
spaced.add(children[i]);
|
||||
}
|
||||
return spaced;
|
||||
}
|
||||
}
|
||||
@ -48,8 +48,6 @@ class SVGAHeadwearWidget extends StatefulWidget {
|
||||
class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
SVGAAnimationController? _animationController;
|
||||
MovieEntity? _retainedMovieEntity;
|
||||
String? _retainedResource;
|
||||
bool _isLoading = true;
|
||||
bool _isNetworkResource = false;
|
||||
bool _hasError = false;
|
||||
@ -84,53 +82,41 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
||||
});
|
||||
|
||||
try {
|
||||
// 检查缓存
|
||||
MovieEntity? videoItem;
|
||||
if (widget.useCache) {
|
||||
final videoItem =
|
||||
_retainedResource == widget.resource && _retainedMovieEntity != null
|
||||
? _retainedMovieEntity!
|
||||
: await SCGiftVapSvgaManager().acquireSvgaEntity(
|
||||
widget.resource,
|
||||
);
|
||||
final acquiredNewEntity = !identical(videoItem, _retainedMovieEntity);
|
||||
if (!mounted) {
|
||||
if (acquiredNewEntity) {
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(videoItem);
|
||||
}
|
||||
return;
|
||||
}
|
||||
videoItem = SCGiftVapSvgaManager().videoItemCache[widget.resource];
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_releaseRetainedMovieEntity(except: videoItem);
|
||||
_retainedMovieEntity = videoItem;
|
||||
_retainedResource = widget.resource;
|
||||
_animationController?.videoItem = videoItem;
|
||||
_isLoading = false;
|
||||
});
|
||||
} else {
|
||||
final videoItem =
|
||||
// 如果没有缓存,则进行解析
|
||||
if (videoItem == null) {
|
||||
videoItem =
|
||||
_isNetworkResource
|
||||
? await SVGAParser.shared.decodeFromURL(widget.resource)
|
||||
: await SVGAParser.shared.decodeFromAssets(widget.resource);
|
||||
if (!mounted) {
|
||||
videoItem.dispose();
|
||||
return;
|
||||
videoItem.autorelease = false;
|
||||
// 存入缓存
|
||||
if (widget.useCache) {
|
||||
SCGiftVapSvgaManager().videoItemCache[widget.resource] = videoItem;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_animationController?.videoItem = videoItem;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 根据循环次数设置播放方式
|
||||
if (widget.loops == 0) {
|
||||
_animationController?.repeat();
|
||||
} else {
|
||||
_animationController?.repeat(count: widget.loops);
|
||||
}
|
||||
// 根据循环次数设置播放方式
|
||||
if (widget.loops == 0) {
|
||||
_animationController?.repeat();
|
||||
} else {
|
||||
_animationController?.repeat(count: widget.loops);
|
||||
}
|
||||
|
||||
if (widget.onFinishLoading != null) {
|
||||
widget.onFinishLoading!();
|
||||
if (widget.onFinishLoading != null) {
|
||||
widget.onFinishLoading!();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@ -171,16 +157,14 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
||||
_animationController?.stop();
|
||||
if (widget.clearsAfterStop) {
|
||||
_animationController?.videoItem = null;
|
||||
_releaseRetainedMovieEntity();
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载动画
|
||||
void reload() {
|
||||
if (widget.useCache) {
|
||||
_animationController?.videoItem = null;
|
||||
_releaseRetainedMovieEntity();
|
||||
SCGiftVapSvgaManager().evictSvgaEntity(widget.resource);
|
||||
// 清除缓存项
|
||||
SCGiftVapSvgaManager().videoItemCache.remove(widget.resource);
|
||||
}
|
||||
_loadAnimation();
|
||||
}
|
||||
@ -189,20 +173,9 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
||||
void dispose() {
|
||||
stop();
|
||||
_animationController?.dispose();
|
||||
_releaseRetainedMovieEntity();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _releaseRetainedMovieEntity({MovieEntity? except}) {
|
||||
final entity = _retainedMovieEntity;
|
||||
if (entity == null || identical(entity, except)) {
|
||||
return;
|
||||
}
|
||||
_retainedMovieEntity = null;
|
||||
_retainedResource = null;
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(entity);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading && !widget.autoPlay) {
|
||||
|
||||
@ -270,8 +270,6 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
bool _isLoading = false;
|
||||
bool _hasError = false;
|
||||
String? _currentResource;
|
||||
MovieEntity? _retainedMovieEntity;
|
||||
String? _retainedResource;
|
||||
|
||||
// 用于存储SVGAImage组件的key,以便刷新占位符
|
||||
GlobalKey _svgaKey = GlobalKey();
|
||||
@ -353,79 +351,45 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
_hasError = false;
|
||||
});
|
||||
|
||||
MovieEntity? acquiredMovieEntity;
|
||||
try {
|
||||
final isNetworkResource = resource.startsWith('http');
|
||||
MovieEntity? videoItem;
|
||||
|
||||
if (widget.useCache) {
|
||||
final videoItem =
|
||||
_retainedResource == resource && _retainedMovieEntity != null
|
||||
? _retainedMovieEntity!
|
||||
: await SCGiftVapSvgaManager().acquireSvgaEntity(resource);
|
||||
final acquiredNewEntity = !identical(videoItem, _retainedMovieEntity);
|
||||
if (acquiredNewEntity) {
|
||||
acquiredMovieEntity = videoItem;
|
||||
}
|
||||
if (!mounted) {
|
||||
if (acquiredNewEntity) {
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(videoItem);
|
||||
acquiredMovieEntity = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
videoItem = SCGiftVapSvgaManager().videoItemCache[resource];
|
||||
}
|
||||
|
||||
// 应用动态数据(文本、图片替换)
|
||||
await _applyDynamicData(videoItem, dynamicData);
|
||||
if (!mounted) {
|
||||
if (acquiredNewEntity) {
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(videoItem);
|
||||
acquiredMovieEntity = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_releaseRetainedMovieEntity(except: videoItem);
|
||||
_retainedMovieEntity = videoItem;
|
||||
_retainedResource = resource;
|
||||
_animationController?.videoItem = videoItem;
|
||||
_isLoading = false;
|
||||
});
|
||||
acquiredMovieEntity = null;
|
||||
} else {
|
||||
final isNetworkResource = resource.startsWith('http');
|
||||
final videoItem =
|
||||
if (videoItem == null) {
|
||||
videoItem =
|
||||
isNetworkResource
|
||||
? await SVGAParser.shared.decodeFromURL(resource)
|
||||
: await SVGAParser.shared.decodeFromAssets(resource);
|
||||
if (!mounted) {
|
||||
videoItem.dispose();
|
||||
return;
|
||||
}
|
||||
videoItem.autorelease = false;
|
||||
|
||||
if (widget.useCache) {
|
||||
SCGiftVapSvgaManager().videoItemCache[resource] = videoItem;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// 应用动态数据(文本、图片替换)
|
||||
await _applyDynamicData(videoItem, dynamicData);
|
||||
if (!mounted) {
|
||||
videoItem.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_animationController?.videoItem = videoItem;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 开始播放动画
|
||||
_startAnimation();
|
||||
// 开始播放动画
|
||||
_startAnimation();
|
||||
|
||||
if (widget.onFinishLoading != null) {
|
||||
widget.onFinishLoading!();
|
||||
if (widget.onFinishLoading != null) {
|
||||
widget.onFinishLoading!();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_animationController?.videoItem = null;
|
||||
_releaseRetainedMovieEntity();
|
||||
_isLoading = false;
|
||||
_hasError = true;
|
||||
});
|
||||
@ -579,7 +543,6 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
_animationController?.stop();
|
||||
if (widget.clearsAfterStop) {
|
||||
_animationController?.videoItem = null;
|
||||
_releaseRetainedMovieEntity();
|
||||
setState(() {
|
||||
_currentResource = null;
|
||||
});
|
||||
@ -590,9 +553,7 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
void reload() {
|
||||
if (_currentResource != null) {
|
||||
if (widget.useCache) {
|
||||
_animationController?.videoItem = null;
|
||||
_releaseRetainedMovieEntity();
|
||||
SCGiftVapSvgaManager().evictSvgaEntity(_currentResource!);
|
||||
SCGiftVapSvgaManager().videoItemCache.remove(_currentResource);
|
||||
}
|
||||
|
||||
_loadAnimation(
|
||||
@ -607,22 +568,11 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
void dispose() {
|
||||
stop();
|
||||
_animationController?.dispose();
|
||||
_releaseRetainedMovieEntity();
|
||||
// 从队列管理器中移除当前播放器
|
||||
_queueManager.clearCurrentPlayer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _releaseRetainedMovieEntity({MovieEntity? except}) {
|
||||
final entity = _retainedMovieEntity;
|
||||
if (entity == null || identical(entity, except)) {
|
||||
return;
|
||||
}
|
||||
_retainedMovieEntity = null;
|
||||
_retainedResource = null;
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(entity);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 显示加载状态
|
||||
|
||||
728
lib/ui_kit/widgets/room/cp/room_cp_invite_dialog.dart
Normal file
@ -0,0 +1,728 @@
|
||||
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/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
|
||||
const String _roomCpInviteAssetBase = 'sc_images/room/cp_invite';
|
||||
|
||||
enum RoomCpInviteDialogStyle { waiting, incoming }
|
||||
|
||||
class RoomCpInviteDialogUser {
|
||||
const RoomCpInviteDialogUser({required this.name, this.avatarUrl = ''});
|
||||
|
||||
final String name;
|
||||
final String avatarUrl;
|
||||
}
|
||||
|
||||
class RoomCpInviteDialog extends StatefulWidget {
|
||||
const RoomCpInviteDialog({
|
||||
super.key,
|
||||
required this.inviter,
|
||||
required this.receiver,
|
||||
this.style = RoomCpInviteDialogStyle.waiting,
|
||||
this.countdownSeconds = 86399,
|
||||
this.dismissTag = dialogTag,
|
||||
this.autoDismiss = true,
|
||||
this.dismissOnAction = true,
|
||||
this.titleText,
|
||||
this.descriptionText,
|
||||
this.bottomText,
|
||||
this.acceptText = 'Accept',
|
||||
this.rejectText = 'Reject',
|
||||
this.onAccept,
|
||||
this.onReject,
|
||||
this.onClose,
|
||||
this.onTimeout,
|
||||
});
|
||||
|
||||
static const String dialogTag = 'showRoomCpInviteDialog';
|
||||
|
||||
final RoomCpInviteDialogUser inviter;
|
||||
final RoomCpInviteDialogUser receiver;
|
||||
final RoomCpInviteDialogStyle style;
|
||||
final int countdownSeconds;
|
||||
final String dismissTag;
|
||||
final bool autoDismiss;
|
||||
final bool dismissOnAction;
|
||||
final String? titleText;
|
||||
final String? descriptionText;
|
||||
final String? bottomText;
|
||||
final String acceptText;
|
||||
final String rejectText;
|
||||
final VoidCallback? onAccept;
|
||||
final VoidCallback? onReject;
|
||||
final VoidCallback? onClose;
|
||||
final VoidCallback? onTimeout;
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required RoomCpInviteDialogUser inviter,
|
||||
required RoomCpInviteDialogUser receiver,
|
||||
RoomCpInviteDialogStyle style = RoomCpInviteDialogStyle.waiting,
|
||||
int countdownSeconds = 86399,
|
||||
String dismissTag = dialogTag,
|
||||
bool autoDismiss = true,
|
||||
bool dismissOnAction = true,
|
||||
String? titleText,
|
||||
String? descriptionText,
|
||||
String? bottomText,
|
||||
String acceptText = 'Accept',
|
||||
String rejectText = 'Reject',
|
||||
VoidCallback? onAccept,
|
||||
VoidCallback? onReject,
|
||||
VoidCallback? onClose,
|
||||
VoidCallback? onTimeout,
|
||||
}) async {
|
||||
await SmartDialog.dismiss(tag: dismissTag);
|
||||
await SmartDialog.show(
|
||||
tag: dismissTag,
|
||||
alignment: Alignment.center,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
builder: (_) {
|
||||
return RoomCpInviteDialog(
|
||||
inviter: inviter,
|
||||
receiver: receiver,
|
||||
style: style,
|
||||
countdownSeconds: countdownSeconds,
|
||||
dismissTag: dismissTag,
|
||||
autoDismiss: autoDismiss,
|
||||
dismissOnAction: dismissOnAction,
|
||||
titleText: titleText,
|
||||
descriptionText: descriptionText,
|
||||
bottomText: bottomText,
|
||||
acceptText: acceptText,
|
||||
rejectText: rejectText,
|
||||
onAccept: onAccept,
|
||||
onReject: onReject,
|
||||
onClose: onClose,
|
||||
onTimeout: onTimeout,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<RoomCpInviteDialog> createState() => _RoomCpInviteDialogState();
|
||||
}
|
||||
|
||||
class _RoomCpInviteDialogState extends State<RoomCpInviteDialog> {
|
||||
static const Color _primaryTextColor = Color(0xff543356);
|
||||
static const Color _secondaryTextColor = Color(0xff67576F);
|
||||
static const Color _pinkTextColor = Color(0xffFF2A70);
|
||||
|
||||
Timer? _timer;
|
||||
late int _secondsLeft;
|
||||
|
||||
bool get _isIncoming => widget.style == RoomCpInviteDialogStyle.incoming;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_secondsLeft = widget.countdownSeconds.clamp(0, 359999).toInt();
|
||||
if (widget.autoDismiss && _secondsLeft > 0) {
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (_secondsLeft <= 1) {
|
||||
_timer?.cancel();
|
||||
widget.onTimeout?.call();
|
||||
SmartDialog.dismiss(tag: widget.dismissTag);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_secondsLeft -= 1;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = ScreenUtil().screenWidth;
|
||||
final screenHeight = ScreenUtil().screenHeight;
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SizedBox(
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ColoredBox(color: Colors.black.withValues(alpha: 0.6)),
|
||||
),
|
||||
_buildDialogBackground(),
|
||||
_buildTitle(),
|
||||
_buildGift(),
|
||||
_buildAvatars(),
|
||||
_buildUserNames(),
|
||||
_buildContentPanel(),
|
||||
_buildBottomContent(),
|
||||
_buildCloseButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDialogBackground() {
|
||||
return Positioned(
|
||||
left: 18.w,
|
||||
top: 150.w,
|
||||
child: Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_background.png',
|
||||
width: 340.w,
|
||||
height: 458.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle() {
|
||||
return Positioned(
|
||||
left: 83.w,
|
||||
top: 221.w,
|
||||
child: Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_title.png',
|
||||
width: 221.w,
|
||||
height: 60.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGift() {
|
||||
return Positioned.fill(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 135.w,
|
||||
top: 277.w,
|
||||
child: SizedBox(
|
||||
width: 106.w,
|
||||
height: 70.w,
|
||||
child: Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_gift_burst.png',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 148.w,
|
||||
top: 333.w,
|
||||
child: Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_gift_floor.png',
|
||||
width: 80.w,
|
||||
height: 41.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 166.w,
|
||||
top: 310.w,
|
||||
child: Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_gift.png',
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatars() {
|
||||
return Positioned.fill(
|
||||
child: Stack(
|
||||
children: [
|
||||
_PositionedAvatar(
|
||||
left: 76.w,
|
||||
top: 296.w,
|
||||
avatarLeft: 78.w,
|
||||
avatarTop: 301.w,
|
||||
user: widget.inviter,
|
||||
),
|
||||
_PositionedAvatar(
|
||||
left: 241.w,
|
||||
top: 296.w,
|
||||
avatarLeft: 243.w,
|
||||
avatarTop: 301.w,
|
||||
user: widget.receiver,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserNames() {
|
||||
return Stack(
|
||||
children: [
|
||||
_PositionedText(
|
||||
left: 77.w,
|
||||
top: 370.w,
|
||||
width: 56.w,
|
||||
height: 10.w,
|
||||
text: _safeName(widget.inviter.name),
|
||||
fontSize: 10.sp,
|
||||
color: _primaryTextColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
_PositionedText(
|
||||
left: 243.w,
|
||||
top: 370.w,
|
||||
width: 56.w,
|
||||
height: 10.w,
|
||||
text: _safeName(widget.receiver.name),
|
||||
fontSize: 10.sp,
|
||||
color: _primaryTextColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContentPanel() {
|
||||
return Positioned(
|
||||
left: 58.w,
|
||||
top: 398.w,
|
||||
child: SizedBox(
|
||||
width: 260.w,
|
||||
height: 111.w,
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_panel.png',
|
||||
width: 260.w,
|
||||
height: 111.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Positioned(
|
||||
left: 42.w,
|
||||
top: 13.w,
|
||||
child: SizedBox(
|
||||
width: 176.w,
|
||||
height: 16.w,
|
||||
child: Text(
|
||||
_panelTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: _primaryTextColor,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 45.w,
|
||||
top: 37.w,
|
||||
child: SizedBox(
|
||||
width: 170.w,
|
||||
height: 12.w,
|
||||
child: _DescriptionText(text: _descriptionText),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 50.w,
|
||||
top: 59.w,
|
||||
child: Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_divider.png',
|
||||
width: 160.w,
|
||||
height: 11.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 65.w,
|
||||
top: 74.w,
|
||||
child: _CountdownRow(text: _countdownText),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomContent() {
|
||||
if (_isIncoming) {
|
||||
return Positioned(
|
||||
left: 73.w,
|
||||
top: 536.w,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ActionButton(
|
||||
asset: '$_roomCpInviteAssetBase/sc_cp_invite_reject_btn.png',
|
||||
label: widget.rejectText,
|
||||
width: 108.w,
|
||||
height: 34.w,
|
||||
color: _pinkTextColor,
|
||||
onTap: _handleReject,
|
||||
),
|
||||
SizedBox(width: 14.w),
|
||||
_ActionButton(
|
||||
asset: '$_roomCpInviteAssetBase/sc_cp_invite_accept_btn.png',
|
||||
label: widget.acceptText,
|
||||
width: 108.w,
|
||||
height: 34.w,
|
||||
color: Colors.white,
|
||||
leadingPadding: 22.w,
|
||||
onTap: _handleAccept,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _PositionedText(
|
||||
left: 90.w,
|
||||
top: 536.w,
|
||||
width: 196.w,
|
||||
height: 12.w,
|
||||
text: widget.bottomText ?? 'Wait for the other party to accept...',
|
||||
fontSize: 12.sp,
|
||||
color: _primaryTextColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCloseButton() {
|
||||
return Positioned(
|
||||
left: 168.w,
|
||||
top: 627.w,
|
||||
child: SCDebounceWidget(
|
||||
onTap: _handleClose,
|
||||
child: Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_close.png',
|
||||
width: 40.w,
|
||||
height: 51.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String get _panelTitle {
|
||||
final title = widget.titleText?.trim();
|
||||
if (title != null && title.isNotEmpty) {
|
||||
return title;
|
||||
}
|
||||
return _safeName(_isIncoming ? widget.inviter.name : widget.receiver.name);
|
||||
}
|
||||
|
||||
String get _descriptionText {
|
||||
final description = widget.descriptionText?.trim();
|
||||
if (description != null && description.isNotEmpty) {
|
||||
return description;
|
||||
}
|
||||
return _isIncoming
|
||||
? 'Invite you to become Couple'
|
||||
: 'We invite you to become Couple';
|
||||
}
|
||||
|
||||
String get _countdownText {
|
||||
final hours = (_secondsLeft ~/ 3600).toString().padLeft(2, '0');
|
||||
final minutes = ((_secondsLeft % 3600) ~/ 60).toString().padLeft(2, '0');
|
||||
final seconds = (_secondsLeft % 60).toString().padLeft(2, '0');
|
||||
return 'Countdown: $hours:$minutes:$seconds';
|
||||
}
|
||||
|
||||
String _safeName(String value) {
|
||||
final trimmed = value.trim();
|
||||
return trimmed.isEmpty ? 'NameName' : trimmed;
|
||||
}
|
||||
|
||||
void _handleAccept() {
|
||||
widget.onAccept?.call();
|
||||
_dismissAfterAction();
|
||||
}
|
||||
|
||||
void _handleReject() {
|
||||
widget.onReject?.call();
|
||||
_dismissAfterAction();
|
||||
}
|
||||
|
||||
void _handleClose() {
|
||||
widget.onClose?.call();
|
||||
SmartDialog.dismiss(tag: widget.dismissTag);
|
||||
}
|
||||
|
||||
void _dismissAfterAction() {
|
||||
if (widget.dismissOnAction) {
|
||||
SmartDialog.dismiss(tag: widget.dismissTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PositionedAvatar extends StatelessWidget {
|
||||
const _PositionedAvatar({
|
||||
required this.left,
|
||||
required this.top,
|
||||
required this.avatarLeft,
|
||||
required this.avatarTop,
|
||||
required this.user,
|
||||
});
|
||||
|
||||
final double left;
|
||||
final double top;
|
||||
final double avatarLeft;
|
||||
final double avatarTop;
|
||||
final RoomCpInviteDialogUser user;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned.fill(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: avatarLeft,
|
||||
top: avatarTop,
|
||||
child: ClipOval(child: _AvatarImage(user: user)),
|
||||
),
|
||||
Positioned(
|
||||
left: left,
|
||||
top: top,
|
||||
child: Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_avatar_frame.png',
|
||||
width: 59.w,
|
||||
height: 69.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AvatarImage extends StatelessWidget {
|
||||
const _AvatarImage({required this.user});
|
||||
|
||||
final RoomCpInviteDialogUser user;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (user.avatarUrl.trim().isNotEmpty) {
|
||||
return netImage(
|
||||
url: user.avatarUrl,
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
fit: BoxFit.cover,
|
||||
defaultImg: 'sc_images/general/sc_icon_avar_defalt.png',
|
||||
);
|
||||
}
|
||||
return Image.asset(
|
||||
'sc_images/general/sc_icon_avar_defalt.png',
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PositionedText extends StatelessWidget {
|
||||
const _PositionedText({
|
||||
required this.left,
|
||||
required this.top,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.text,
|
||||
required this.fontSize,
|
||||
required this.color,
|
||||
required this.fontWeight,
|
||||
});
|
||||
|
||||
final double left;
|
||||
final double top;
|
||||
final double width;
|
||||
final double height;
|
||||
final String text;
|
||||
final double fontSize;
|
||||
final Color color;
|
||||
final FontWeight fontWeight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
left: left,
|
||||
top: top,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DescriptionText extends StatelessWidget {
|
||||
const _DescriptionText({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const couple = 'Couple';
|
||||
final coupleIndex = text.indexOf(couple);
|
||||
if (coupleIndex < 0) {
|
||||
return Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: _style(_RoomCpInviteDialogState._secondaryTextColor),
|
||||
);
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: text.substring(0, coupleIndex)),
|
||||
TextSpan(
|
||||
text: couple,
|
||||
style: _style(_RoomCpInviteDialogState._pinkTextColor),
|
||||
),
|
||||
TextSpan(text: text.substring(coupleIndex + couple.length)),
|
||||
],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: _style(_RoomCpInviteDialogState._secondaryTextColor),
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle _style(Color color) {
|
||||
return TextStyle(
|
||||
color: color,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountdownRow extends StatelessWidget {
|
||||
const _CountdownRow({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 130.w,
|
||||
height: 24.w,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'$_roomCpInviteAssetBase/sc_cp_invite_countdown.png',
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: _RoomCpInviteDialogState._pinkTextColor,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionButton extends StatelessWidget {
|
||||
const _ActionButton({
|
||||
required this.asset,
|
||||
required this.label,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
this.leadingPadding = 0,
|
||||
});
|
||||
|
||||
final String asset;
|
||||
final String label;
|
||||
final double width;
|
||||
final double height;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
final double leadingPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCDebounceWidget(
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset(asset, width: width, height: height, fit: BoxFit.fill),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: leadingPadding),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
488
lib/ui_kit/widgets/room/cp/room_cp_progress_dialog.dart
Normal file
@ -0,0 +1,488 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
|
||||
const String _roomCpProgressAssetBase = 'sc_images/room/cp_progress';
|
||||
|
||||
/// User payload reserved for the voice-room CP progress API.
|
||||
class RoomCpProgressDialogUser {
|
||||
const RoomCpProgressDialogUser({
|
||||
this.nickname = '',
|
||||
this.userId = '',
|
||||
this.avatarUrl = '',
|
||||
this.levelText = '',
|
||||
});
|
||||
|
||||
final String nickname;
|
||||
final String userId;
|
||||
final String avatarUrl;
|
||||
final String levelText;
|
||||
}
|
||||
|
||||
/// Voice-room CP progress popup.
|
||||
class RoomCpProgressDialog extends StatelessWidget {
|
||||
const RoomCpProgressDialog({
|
||||
super.key,
|
||||
required this.leftUser,
|
||||
required this.rightUser,
|
||||
this.daysValue = '999',
|
||||
this.daysLabel = 'Days',
|
||||
this.levelText = 'Lv.1 Simple Love',
|
||||
this.currentValue = 1,
|
||||
this.targetValue = 1,
|
||||
this.expAwayText = '1,000',
|
||||
this.nextLevelText = 'Lv. 2',
|
||||
this.cancelText = 'Cancel',
|
||||
this.dismissTag = dialogTag,
|
||||
this.onCancel,
|
||||
});
|
||||
|
||||
static const String dialogTag = 'showRoomCpProgressDialog';
|
||||
|
||||
final RoomCpProgressDialogUser leftUser;
|
||||
final RoomCpProgressDialogUser rightUser;
|
||||
final String daysValue;
|
||||
final String daysLabel;
|
||||
final String levelText;
|
||||
final double currentValue;
|
||||
final double targetValue;
|
||||
final String expAwayText;
|
||||
final String nextLevelText;
|
||||
final String cancelText;
|
||||
final String dismissTag;
|
||||
final VoidCallback? onCancel;
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required RoomCpProgressDialogUser leftUser,
|
||||
required RoomCpProgressDialogUser rightUser,
|
||||
String daysValue = '999',
|
||||
String daysLabel = 'Days',
|
||||
String levelText = 'Lv.1 Simple Love',
|
||||
double currentValue = 1,
|
||||
double targetValue = 1,
|
||||
String expAwayText = '1,000',
|
||||
String nextLevelText = 'Lv. 2',
|
||||
String cancelText = 'Cancel',
|
||||
String dismissTag = dialogTag,
|
||||
VoidCallback? onCancel,
|
||||
}) async {
|
||||
await SmartDialog.dismiss(tag: dismissTag);
|
||||
await SmartDialog.show(
|
||||
tag: dismissTag,
|
||||
alignment: Alignment.center,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.black.withValues(alpha: 0.6),
|
||||
builder:
|
||||
(_) => RoomCpProgressDialog(
|
||||
leftUser: leftUser,
|
||||
rightUser: rightUser,
|
||||
daysValue: daysValue,
|
||||
daysLabel: daysLabel,
|
||||
levelText: levelText,
|
||||
currentValue: currentValue,
|
||||
targetValue: targetValue,
|
||||
expAwayText: expAwayText,
|
||||
nextLevelText: nextLevelText,
|
||||
cancelText: cancelText,
|
||||
dismissTag: dismissTag,
|
||||
onCancel: onCancel,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double get _progressFraction {
|
||||
if (targetValue <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (currentValue / targetValue).clamp(0.0, 1.0).toDouble();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Center(
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -42.w),
|
||||
child: SizedBox(
|
||||
width: 375.w,
|
||||
height: 275.w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 18.w,
|
||||
top: 0,
|
||||
child: Image.asset(
|
||||
'$_roomCpProgressAssetBase/sc_cp_progress_dialog_bg.png',
|
||||
width: 340.w,
|
||||
height: 275.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 131.w,
|
||||
top: 46.w,
|
||||
child: _DaysBadge(
|
||||
daysValue: daysValue,
|
||||
daysLabel: daysLabel,
|
||||
levelText: levelText,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 71.w,
|
||||
top: 51.w,
|
||||
child: _ProgressAvatar(user: leftUser),
|
||||
),
|
||||
Positioned(
|
||||
left: 246.w,
|
||||
top: 51.w,
|
||||
child: _ProgressAvatar(user: rightUser),
|
||||
),
|
||||
Positioned(
|
||||
left: 60.w,
|
||||
top: 122.w,
|
||||
child: _UserName(user: leftUser),
|
||||
),
|
||||
Positioned(
|
||||
left: 236.w,
|
||||
top: 122.w,
|
||||
child: _UserName(user: rightUser),
|
||||
),
|
||||
Positioned(
|
||||
left: 58.w,
|
||||
top: 146.w,
|
||||
child: _ProgressBar(fraction: _progressFraction),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 180.w,
|
||||
child: _ExpAwayText(
|
||||
expAwayText: expAwayText,
|
||||
nextLevelText: nextLevelText,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 130.w,
|
||||
top: 206.w,
|
||||
child: SCDebounceWidget(
|
||||
onTap: () {
|
||||
onCancel?.call();
|
||||
SmartDialog.dismiss(tag: dismissTag);
|
||||
},
|
||||
child: _CancelButton(text: cancelText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DaysBadge extends StatelessWidget {
|
||||
const _DaysBadge({
|
||||
required this.daysValue,
|
||||
required this.daysLabel,
|
||||
required this.levelText,
|
||||
});
|
||||
|
||||
final String daysValue;
|
||||
final String daysLabel;
|
||||
final String levelText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = daysValue.trim().isEmpty ? '--' : daysValue.trim();
|
||||
final label = daysLabel.trim().isEmpty ? 'Days' : daysLabel.trim();
|
||||
final level =
|
||||
levelText.trim().isEmpty ? 'Lv.1 Simple Love' : levelText.trim();
|
||||
|
||||
return SizedBox(
|
||||
width: 114.w,
|
||||
height: 87.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Image.asset(
|
||||
'$_roomCpProgressAssetBase/sc_cp_progress_header.png',
|
||||
width: 114.w,
|
||||
height: 87.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Positioned(
|
||||
top: 18.w,
|
||||
child: SizedBox(
|
||||
width: 68.w,
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xffFEFEFF),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 39.w,
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xffFEFEFF),
|
||||
fontSize: 8.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 65.w,
|
||||
child: SizedBox(
|
||||
width: 82.w,
|
||||
child: Text(
|
||||
level,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xffFEFEFF),
|
||||
fontSize: 8.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressAvatar extends StatelessWidget {
|
||||
const _ProgressAvatar({required this.user});
|
||||
|
||||
final RoomCpProgressDialogUser user;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 59.w,
|
||||
height: 69.w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Positioned(left: 2.w, top: 5.w, child: _AvatarImage(user: user)),
|
||||
Image.asset(
|
||||
'$_roomCpProgressAssetBase/sc_cp_progress_avatar_frame.png',
|
||||
width: 59.w,
|
||||
height: 69.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AvatarImage extends StatelessWidget {
|
||||
const _AvatarImage({required this.user});
|
||||
|
||||
final RoomCpProgressDialogUser user;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (user.avatarUrl.trim().isNotEmpty) {
|
||||
return ClipOval(
|
||||
child: netImage(
|
||||
url: user.avatarUrl,
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
fit: BoxFit.cover,
|
||||
defaultImg: 'sc_images/general/sc_icon_avar_defalt.png',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ClipOval(
|
||||
child: Image.asset(
|
||||
'sc_images/general/sc_icon_avar_defalt.png',
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserName extends StatelessWidget {
|
||||
const _UserName({required this.user});
|
||||
|
||||
final RoomCpProgressDialogUser user;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final name =
|
||||
user.nickname.trim().isEmpty ? 'NameName' : user.nickname.trim();
|
||||
|
||||
return SizedBox(
|
||||
width: 80.w,
|
||||
height: 12.w,
|
||||
child: Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: const Color(0xff543356),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressBar extends StatelessWidget {
|
||||
const _ProgressBar({required this.fraction});
|
||||
|
||||
final double fraction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final safeFraction = math.max(0.0, math.min(1.0, fraction));
|
||||
|
||||
return SizedBox(
|
||||
width: 260.w,
|
||||
height: 26.w,
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
'$_roomCpProgressAssetBase/sc_cp_progress_bar_bg.png',
|
||||
width: 260.w,
|
||||
height: 26.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Positioned(
|
||||
left: 9.w,
|
||||
top: 5.w,
|
||||
child: ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: safeFraction,
|
||||
child: Image.asset(
|
||||
'$_roomCpProgressAssetBase/sc_cp_progress_bar_fill.png',
|
||||
width: 242.w,
|
||||
height: 16.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExpAwayText extends StatelessWidget {
|
||||
const _ExpAwayText({required this.expAwayText, required this.nextLevelText});
|
||||
|
||||
final String expAwayText;
|
||||
final String nextLevelText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final exp = expAwayText.trim().isEmpty ? '1,000' : expAwayText.trim();
|
||||
final nextLevel =
|
||||
nextLevelText.trim().isEmpty ? 'Lv. 2' : nextLevelText.trim();
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$exp ',
|
||||
style: const TextStyle(color: Color(0xffFF2A70)),
|
||||
),
|
||||
const TextSpan(
|
||||
text: 'EXP away from ',
|
||||
style: TextStyle(color: Color(0xff67576F)),
|
||||
),
|
||||
TextSpan(
|
||||
text: nextLevel,
|
||||
style: const TextStyle(color: Color(0xffFF2A70)),
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CancelButton extends StatelessWidget {
|
||||
const _CancelButton({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 115.w,
|
||||
height: 36.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'$_roomCpProgressAssetBase/sc_cp_progress_cancel_btn.png',
|
||||
width: 115.w,
|
||||
height: 36.w,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Text(
|
||||
text.trim().isEmpty ? 'Cancel' : text.trim(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -247,7 +247,7 @@ class EmptyMaiSelect extends StatelessWidget {
|
||||
context,
|
||||
() {
|
||||
Navigator.of(context).pop();
|
||||
showBottomInCenterDialog(
|
||||
showBottomInBottomDialog(
|
||||
context,
|
||||
RoomUserInfoCard(userId: clickUser?.id),
|
||||
);
|
||||
|
||||
@ -74,11 +74,16 @@ class _ExitMinRoomPageState extends State<ExitMinRoomPage> {
|
||||
final giftAnimationManager =
|
||||
context.read<GiftAnimationManager>();
|
||||
final rtmProvider = context.read<RtmProvider>();
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id
|
||||
?.trim() ??
|
||||
'';
|
||||
rtcProvider.setVoiceRoomRouteVisible(false);
|
||||
rtcProvider.setRoomVisualEffectsEnabled(false);
|
||||
rtmProvider.msgFloatingGiftListener = null;
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener = null;
|
||||
RoomEntranceHelper.clearQueue();
|
||||
OverlayManager().removeRoom();
|
||||
OverlayManager().removeRoom(roomId: currentRoomId);
|
||||
SCRoomEffectScheduler().clearDeferredTasks(
|
||||
reason: 'room_minimize',
|
||||
);
|
||||
|
||||
@ -1,288 +1,398 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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_room_utils.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
|
||||
class FloatingRoomRocketScreenWidget extends StatefulWidget {
|
||||
final SCFloatingMessage message;
|
||||
final VoidCallback onAnimationCompleted; // 动画完成回调
|
||||
|
||||
const FloatingRoomRocketScreenWidget({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.onAnimationCompleted,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FloatingRoomRocketScreenWidgetState createState() =>
|
||||
_FloatingRoomRocketScreenWidgetState();
|
||||
}
|
||||
|
||||
class _FloatingRoomRocketScreenWidgetState
|
||||
extends State<FloatingRoomRocketScreenWidget>
|
||||
with TickerProviderStateMixin {
|
||||
Debouncer debouncer = Debouncer();
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _firstAnimation; // 第一段:从右到中
|
||||
late Animation<Offset> _secondAnimation; // 第二段:从中到左
|
||||
late AnimationController _swipeController; // 新增:滑动动画控制器
|
||||
late Animation<Offset> _swipeAnimation; // 新增:滑动动画
|
||||
|
||||
bool _firstPhaseCompleted = false;
|
||||
bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 主动画控制器
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500), // 每段动画1.5秒
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// 滑动动画控制器
|
||||
_swipeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 550), // 滑动动画500ms
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// 监听滑动动画完成
|
||||
_swipeController.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
widget.onAnimationCompleted();
|
||||
}
|
||||
});
|
||||
|
||||
// 第一段动画:从右侧飘到中间
|
||||
_firstAnimation = Tween<Offset>(
|
||||
begin: const Offset(1.5, 0.0), // 从屏幕右侧外开始
|
||||
end: Offset.zero, // 到屏幕中心
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
|
||||
|
||||
// 第二段动画:从中间飘到左侧
|
||||
_secondAnimation = Tween<Offset>(
|
||||
begin: Offset.zero, // 从屏幕中心开始
|
||||
end: const Offset(-1.5, 0.0), // 到屏幕左侧外
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
|
||||
|
||||
// 滑动动画:从当前位置滑动到左侧屏幕外
|
||||
_swipeAnimation = Tween<Offset>(
|
||||
begin: Offset.zero, // 从当前位置开始
|
||||
end: const Offset(-1.5, 0.0), // 到屏幕左侧外
|
||||
).animate(CurvedAnimation(
|
||||
parent: _swipeController,
|
||||
curve: Curves.easeIn,
|
||||
));
|
||||
|
||||
// 延迟启动动画
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_startAnimationSequence();
|
||||
}
|
||||
});
|
||||
}
|
||||
void _startAnimationSequence() async {
|
||||
// 第一阶段:从右到中
|
||||
_controller.reset();
|
||||
await _controller.forward();
|
||||
|
||||
// 停顿2秒(你代码中是3秒,这里保持原样)
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// 第二阶段:从中到左
|
||||
setState(() {
|
||||
_firstPhaseCompleted = true;
|
||||
});
|
||||
_controller.reset();
|
||||
await _controller.forward();
|
||||
|
||||
if (mounted && !_isSwipeAnimating) {
|
||||
widget.onAnimationCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_swipeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 如果正在执行滑动动画,使用滑动动画
|
||||
if (_isSwipeAnimating) {
|
||||
return SlideTransition(
|
||||
position: _swipeAnimation,
|
||||
child: _buildRocketAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
// 否则使用原来的动画
|
||||
return SlideTransition(
|
||||
position: _firstPhaseCompleted ? _secondAnimation : _firstAnimation,
|
||||
child: _buildRocketAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRocketAnimation() {
|
||||
// 使用Positioned.fill确保飘屏覆盖全屏宽度,并定位在特定高度(如顶部100像素)
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
debouncer.debounce(
|
||||
duration: Duration(milliseconds: 350),
|
||||
onDebounce: () {
|
||||
if (widget.message.roomId != null &&
|
||||
widget.message.roomId!.isNotEmpty) {
|
||||
SCRoomUtils.goRoom(
|
||||
widget.message.roomId!,
|
||||
navigatorKey.currentState!.context,
|
||||
fromFloting: true
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
onHorizontalDragEnd: (details) {
|
||||
// 获取当前的文本方向
|
||||
final textDirection = Directionality.of(context);
|
||||
|
||||
// 定义一个根据方向转换速度符号的辅助函数
|
||||
double effectiveVelocity(double velocity) {
|
||||
// 在RTL模式下,反转速度的正负号
|
||||
return textDirection == TextDirection.rtl
|
||||
? -velocity
|
||||
: velocity;
|
||||
}
|
||||
|
||||
double velocity = effectiveVelocity(details.primaryVelocity ?? 0);
|
||||
if (velocity < 0) {
|
||||
///向左滑动,把当前视图向左平移出去
|
||||
_handleSwipeLeft();
|
||||
}
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
alignment: Alignment.topCenter,
|
||||
height: 128.w,
|
||||
width: 290.w,
|
||||
margin: EdgeInsets.only(top: 10.w),
|
||||
padding: EdgeInsetsDirectional.only(start: 3.w, end: 25.w),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
"sc_images/index/sc_icon_rocket_broad_bg_lv${widget.message.rocketLevel}.webp",
|
||||
),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min, // 宽度由内容决定
|
||||
children: [
|
||||
SizedBox(width: 5.w),
|
||||
Container(
|
||||
child: head(
|
||||
url: widget.message.userAvatarUrl ?? "",
|
||||
width: 66.w,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 6.w),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60.w,
|
||||
height: 22.w,
|
||||
child: Marquee(
|
||||
text: "ID:${widget.message.userId} ",
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
color: Colors.yellow,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.w900,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
scrollAxis: Axis.horizontal,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
blankSpace: 20.0,
|
||||
velocity: 40.0,
|
||||
pauseAfterRound: Duration(seconds: 1),
|
||||
accelerationDuration: Duration(seconds: 1),
|
||||
accelerationCurve: Curves.easeOut,
|
||||
decelerationDuration: Duration(
|
||||
milliseconds: 500,
|
||||
),
|
||||
decelerationCurve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.launchedARocket,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -5),
|
||||
child: text(
|
||||
SCAppLocalizations.of(context)!.letGoToWatch,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 17.sp,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 处理向左滑动
|
||||
void _handleSwipeLeft() {
|
||||
if (_isSwipeAnimating) return;
|
||||
|
||||
setState(() {
|
||||
_isSwipeAnimating = true;
|
||||
});
|
||||
|
||||
// 停止主动画
|
||||
_controller.stop();
|
||||
|
||||
// 启动滑动动画
|
||||
_swipeController.reset();
|
||||
_swipeController.forward();
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
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/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
|
||||
class FloatingRoomRocketScreenWidget extends StatefulWidget {
|
||||
final SCFloatingMessage message;
|
||||
final VoidCallback onAnimationCompleted; // 动画完成回调
|
||||
|
||||
const FloatingRoomRocketScreenWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.onAnimationCompleted,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FloatingRoomRocketScreenWidget> createState() =>
|
||||
_FloatingRoomRocketScreenWidgetState();
|
||||
}
|
||||
|
||||
class _FloatingRoomRocketScreenWidgetState
|
||||
extends State<FloatingRoomRocketScreenWidget>
|
||||
with TickerProviderStateMixin {
|
||||
Debouncer debouncer = Debouncer();
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _firstAnimation; // 第一段:从右到中
|
||||
late Animation<Offset> _secondAnimation; // 第二段:从中到左
|
||||
late AnimationController _swipeController; // 新增:滑动动画控制器
|
||||
late Animation<Offset> _swipeAnimation; // 新增:滑动动画
|
||||
|
||||
bool _firstPhaseCompleted = false;
|
||||
bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 主动画控制器
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500), // 每段动画1.5秒
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// 滑动动画控制器
|
||||
_swipeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 550), // 滑动动画500ms
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// 监听滑动动画完成
|
||||
_swipeController.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
widget.onAnimationCompleted();
|
||||
}
|
||||
});
|
||||
|
||||
// 第一段动画:从右侧飘到中间
|
||||
_firstAnimation = Tween<Offset>(
|
||||
begin: const Offset(1.5, 0.0), // 从屏幕右侧外开始
|
||||
end: Offset.zero, // 到屏幕中心
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
|
||||
|
||||
// 第二段动画:从中间飘到左侧
|
||||
_secondAnimation = Tween<Offset>(
|
||||
begin: Offset.zero, // 从屏幕中心开始
|
||||
end: const Offset(-1.5, 0.0), // 到屏幕左侧外
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
|
||||
|
||||
// 滑动动画:从当前位置滑动到左侧屏幕外
|
||||
_swipeAnimation = Tween<Offset>(
|
||||
begin: Offset.zero, // 从当前位置开始
|
||||
end: const Offset(-1.5, 0.0), // 到屏幕左侧外
|
||||
).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn));
|
||||
|
||||
// 延迟启动动画
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_startAnimationSequence();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startAnimationSequence() async {
|
||||
// 第一阶段:从右到中
|
||||
_controller.reset();
|
||||
await _controller.forward();
|
||||
|
||||
await Future.delayed(_holdDuration);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// 第二阶段:从中到左
|
||||
setState(() {
|
||||
_firstPhaseCompleted = true;
|
||||
});
|
||||
_controller.reset();
|
||||
await _controller.forward();
|
||||
|
||||
if (mounted && !_isSwipeAnimating) {
|
||||
widget.onAnimationCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
Duration get _holdDuration {
|
||||
return const Duration(seconds: 7);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_swipeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 如果正在执行滑动动画,使用滑动动画
|
||||
if (_isSwipeAnimating) {
|
||||
return SlideTransition(
|
||||
position: _swipeAnimation,
|
||||
child: _buildRocketAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
// 否则使用原来的动画
|
||||
return SlideTransition(
|
||||
position: _firstPhaseCompleted ? _secondAnimation : _firstAnimation,
|
||||
child: _buildRocketAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRocketAnimation() {
|
||||
final roomName = _firstNonBlank([
|
||||
widget.message.userName,
|
||||
widget.message.userId,
|
||||
widget.message.roomId,
|
||||
]);
|
||||
final rocketIconUrl = _firstNonBlank([
|
||||
widget.message.giftUrl,
|
||||
widget.message.fallbackUrl,
|
||||
]);
|
||||
|
||||
// 使用Positioned.fill确保飘屏覆盖全屏宽度,并定位在特定高度(如顶部100像素)
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
debouncer.debounce(
|
||||
duration: Duration(milliseconds: 350),
|
||||
onDebounce: () {
|
||||
final roomId = widget.message.roomId?.trim() ?? '';
|
||||
final currentContext = navigatorKey.currentState?.context;
|
||||
if (roomId.isEmpty ||
|
||||
currentContext == null ||
|
||||
!currentContext.mounted ||
|
||||
SCRoomUtils.isCurrentRoom(roomId, currentContext)) {
|
||||
return;
|
||||
}
|
||||
if (roomId.isNotEmpty) {
|
||||
SCRoomUtils.goRoom(roomId, currentContext, fromFloting: true);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
onHorizontalDragEnd: (details) {
|
||||
// 获取当前的文本方向
|
||||
final textDirection = Directionality.of(context);
|
||||
|
||||
// 定义一个根据方向转换速度符号的辅助函数
|
||||
double effectiveVelocity(double velocity) {
|
||||
// 在RTL模式下,反转速度的正负号
|
||||
return textDirection == TextDirection.rtl ? -velocity : velocity;
|
||||
}
|
||||
|
||||
double velocity = effectiveVelocity(details.primaryVelocity ?? 0);
|
||||
if (velocity < 0) {
|
||||
///向左滑动,把当前视图向左平移出去
|
||||
_handleSwipeLeft();
|
||||
}
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
width: 307.w,
|
||||
height: 68.w,
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(_RocketFloatingAssets.bannerBg),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 21.w,
|
||||
top: 8.w,
|
||||
width: 38.w,
|
||||
height: 38.w,
|
||||
child: head(
|
||||
url: widget.message.userAvatarUrl ?? '',
|
||||
width: 38.w,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 65.w,
|
||||
top: 9.w,
|
||||
width: 126.w,
|
||||
child: Text(
|
||||
roomName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 65.w,
|
||||
top: 31.w,
|
||||
width: 126.w,
|
||||
child: Text(
|
||||
SCAppLocalizations.of(context)?.launchRocket ??
|
||||
'Launch Rocket',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.61),
|
||||
fontSize: 12.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 201.w,
|
||||
top: 8.w,
|
||||
width: 40.w,
|
||||
height: 40.w,
|
||||
child: _RocketIcon(url: rocketIconUrl),
|
||||
),
|
||||
Positioned(
|
||||
left: 249.w,
|
||||
top: 14.w,
|
||||
width: 42.w,
|
||||
height: 28.w,
|
||||
child: const _RocketGoButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 处理向左滑动
|
||||
void _handleSwipeLeft() {
|
||||
if (_isSwipeAnimating) return;
|
||||
|
||||
setState(() {
|
||||
_isSwipeAnimating = true;
|
||||
});
|
||||
|
||||
// 停止主动画
|
||||
_controller.stop();
|
||||
|
||||
// 启动滑动动画
|
||||
_swipeController.reset();
|
||||
_swipeController.forward();
|
||||
}
|
||||
|
||||
String _firstNonBlank(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
class _RocketIcon extends StatelessWidget {
|
||||
const _RocketIcon({required this.url});
|
||||
|
||||
final String url;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4.w),
|
||||
color: const Color(0xFF08261F),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF54F8D2).withValues(alpha: 0.5),
|
||||
width: 0.7.w,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF14FFC7).withValues(alpha: 0.16),
|
||||
blurRadius: 7.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5.w),
|
||||
child:
|
||||
url.trim().isEmpty
|
||||
? Image.asset(
|
||||
_RocketFloatingAssets.fallbackRocket,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
: netImage(
|
||||
url: url,
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
fit: BoxFit.contain,
|
||||
noDefaultImg: true,
|
||||
errorWidget: Image.asset(
|
||||
_RocketFloatingAssets.fallbackRocket,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RocketGoButton extends StatelessWidget {
|
||||
const _RocketGoButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF0C745B), Color(0xFF013C31)],
|
||||
),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF8DFFE3).withValues(alpha: 0.72),
|
||||
width: 1.w,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF21FFC3).withValues(alpha: 0.28),
|
||||
blurRadius: 6.w,
|
||||
spreadRadius: 0.5.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: ShaderMask(
|
||||
shaderCallback:
|
||||
(bounds) => const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.white, Color(0xFFA4FFE3)],
|
||||
).createShader(bounds),
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Text(
|
||||
'GO',
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RocketFloatingAssets {
|
||||
const _RocketFloatingAssets._();
|
||||
|
||||
static const String bannerBg =
|
||||
'sc_images/room/rocket/rocket_floating_banner_bg.png';
|
||||
static const String fallbackRocket = 'sc_images/room/rocket/rocket_body.png';
|
||||
}
|
||||
|
||||
221
lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart
Normal file
@ -0,0 +1,221 @@
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart';
|
||||
|
||||
class RoomRocketApiMapper {
|
||||
const RoomRocketApiMapper._();
|
||||
|
||||
static List<RoomRocketLevelItem>? levelsFromStatus(
|
||||
SCRoomRocketStatusRes? status,
|
||||
) {
|
||||
final levels = status?.levels;
|
||||
if (levels == null || levels.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final sorted = [...levels]..sort((a, b) {
|
||||
final sortCompare = a.sort.compareTo(b.sort);
|
||||
return sortCompare != 0 ? sortCompare : a.level.compareTo(b.level);
|
||||
});
|
||||
return sorted
|
||||
.map(
|
||||
(item) => RoomRocketLevelItem(
|
||||
label: 'Lv.${item.level}',
|
||||
level: item.level,
|
||||
imageUrl: item.rocketIconUrl.isEmpty ? null : item.rocketIconUrl,
|
||||
previewImageUrl:
|
||||
item.rocketPreviewUrl.isEmpty ? null : item.rocketPreviewUrl,
|
||||
animationUrl:
|
||||
item.rocketAnimationUrl.isEmpty
|
||||
? null
|
||||
: item.rocketAnimationUrl,
|
||||
progressImageUrl:
|
||||
item.progressBarUrl.isEmpty ? null : item.progressBarUrl,
|
||||
shakeThresholdPercent: item.shakeThresholdPercent,
|
||||
enabled: item.enabled,
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
static List<RoomRocketRewardItem>? rewardsFromStatus(
|
||||
SCRoomRocketStatusRes? status,
|
||||
) {
|
||||
final rewards = status?.rewardPreview?.all ?? const [];
|
||||
if (rewards.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return rewards.map(rewardItemFromPreview).toList(growable: false);
|
||||
}
|
||||
|
||||
static RoomRocketRewardGroups? rewardGroupsFromStatus(
|
||||
SCRoomRocketStatusRes? status,
|
||||
) {
|
||||
final preview = status?.rewardPreview;
|
||||
if (preview == null) {
|
||||
return null;
|
||||
}
|
||||
return RoomRocketRewardGroups(
|
||||
top1: preview.top1.map(rewardItemFromPreview).toList(growable: false),
|
||||
setOff: preview.ignite.map(rewardItemFromPreview).toList(growable: false),
|
||||
inRoom: preview.inRoom.map(rewardItemFromPreview).toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
static RoomRocketRewardItem rewardItemFromPreview(
|
||||
SCRoomRocketRewardRes reward,
|
||||
) {
|
||||
return RoomRocketRewardItem(
|
||||
imageUrl: reward.rewardCover.isEmpty ? null : reward.rewardCover,
|
||||
amount: reward.displayAmount,
|
||||
rewardType: reward.rewardType,
|
||||
expireDays: reward.expireDays,
|
||||
);
|
||||
}
|
||||
|
||||
static List<RoomRocketCrewMember>? crewFromStatus(
|
||||
SCRoomRocketStatusRes? status,
|
||||
) {
|
||||
final kings = status?.rocketKings ?? const [];
|
||||
if (kings.isEmpty) {
|
||||
return const <RoomRocketCrewMember>[];
|
||||
}
|
||||
return kings
|
||||
.take(3)
|
||||
.map(
|
||||
(item) => RoomRocketCrewMember(
|
||||
imageUrl: item.userAvatar.isEmpty ? null : item.userAvatar,
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
static List<RoomRocketRewardItem> rewardItemsFromRecords(
|
||||
List<SCRoomRocketRewardRecordRes> records,
|
||||
) {
|
||||
return records.map(_rewardItemFromRecord).toList(growable: false);
|
||||
}
|
||||
|
||||
static List<RoomRocketRewardWinner> winnersFromRecords(
|
||||
List<SCRoomRocketRewardRecordRes> records,
|
||||
) {
|
||||
final grouped = <String, List<SCRoomRocketRewardRecordRes>>{};
|
||||
for (final record in records) {
|
||||
final key = record.userId.isNotEmpty ? record.userId : record.launchNo;
|
||||
grouped.putIfAbsent(key, () => []).add(record);
|
||||
}
|
||||
return grouped.entries
|
||||
.map((entry) {
|
||||
final first = entry.value.first;
|
||||
return RoomRocketRewardWinner(
|
||||
nickname: first.displayName,
|
||||
avatarUrl: first.userAvatar.isEmpty ? null : first.userAvatar,
|
||||
rewards: rewardItemsFromRecords(entry.value),
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
static List<RoomRocketRecordItem> recordItemsFromRecords(
|
||||
List<SCRoomRocketRewardRecordRes> records,
|
||||
) {
|
||||
final grouped = <String, List<SCRoomRocketRewardRecordRes>>{};
|
||||
for (final record in records) {
|
||||
final key = [
|
||||
record.launchNo,
|
||||
record.roomId,
|
||||
record.roundNo,
|
||||
record.grantTime,
|
||||
record.createTime,
|
||||
].join('|');
|
||||
grouped.putIfAbsent(key, () => []).add(record);
|
||||
}
|
||||
return grouped.values
|
||||
.map((items) {
|
||||
final first = items.first;
|
||||
return RoomRocketRecordItem(
|
||||
roomId: 'RoomID:${first.roomId}',
|
||||
levelLabel: _sceneLabel(first.rewardScene),
|
||||
time: _formatRecordTime(first.grantTime, first.createTime),
|
||||
rewards: rewardItemsFromRecords(items),
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
static List<RoomRocketNoteSection>? noteSectionsFromRule(dynamic rule) {
|
||||
final text = _localizedRuleText(rule);
|
||||
if (text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return [RoomRocketNoteSection(title: '', body: text)];
|
||||
}
|
||||
|
||||
static RoomRocketRewardItem _rewardItemFromRecord(
|
||||
SCRoomRocketRewardRecordRes record,
|
||||
) {
|
||||
return RoomRocketRewardItem(
|
||||
imageUrl: record.rewardCover.isEmpty ? null : record.rewardCover,
|
||||
amount: record.displayAmount,
|
||||
rewardType: record.rewardType,
|
||||
expireDays: record.expireDays,
|
||||
);
|
||||
}
|
||||
|
||||
static String _sceneLabel(String scene) {
|
||||
switch (scene.toUpperCase()) {
|
||||
case 'TOP1':
|
||||
return 'Top1';
|
||||
case 'IGNITE':
|
||||
return 'Set off';
|
||||
case 'IN_ROOM':
|
||||
return 'In the room';
|
||||
default:
|
||||
return scene.isEmpty ? 'Reward' : scene;
|
||||
}
|
||||
}
|
||||
|
||||
static String _formatRecordTime(String grantTime, String createTime) {
|
||||
final source = grantTime.isNotEmpty ? grantTime : createTime;
|
||||
final parsed = DateTime.tryParse(source.replaceFirst(' ', 'T'));
|
||||
if (parsed == null) {
|
||||
return source;
|
||||
}
|
||||
String two(int value) => value.toString().padLeft(2, '0');
|
||||
return '${two(parsed.month)}/${two(parsed.day)} ${two(parsed.hour)}:${two(parsed.minute)}';
|
||||
}
|
||||
|
||||
static String _localizedRuleText(dynamic rule) {
|
||||
if (rule is String) {
|
||||
return rule.trim();
|
||||
}
|
||||
if (rule is! Map) {
|
||||
return '';
|
||||
}
|
||||
final lang = SCGlobalConfig.lang;
|
||||
final candidates = [
|
||||
lang,
|
||||
lang.replaceAll('_', '-'),
|
||||
lang.split(RegExp('[-_]')).first,
|
||||
'en',
|
||||
'zh-CN',
|
||||
];
|
||||
for (final key in candidates) {
|
||||
final value = rule[key];
|
||||
final text = value?.toString().trim() ?? '';
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
for (final value in rule.values) {
|
||||
final text = value?.toString().trim() ?? '';
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
1698
lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart
Normal file
651
lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart
Normal file
@ -0,0 +1,651 @@
|
||||
import 'dart:async';
|
||||
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_svg/flutter_svg.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart';
|
||||
|
||||
class RoomRocketFloatingEntry extends StatelessWidget {
|
||||
const RoomRocketFloatingEntry({super.key});
|
||||
|
||||
static const String dialogTag = 'showRoomRocketDialog';
|
||||
static const String rewardDialogTag = 'showRoomRocketRewardDialog';
|
||||
static const String recordDialogTag = 'showRoomRocketRecordDialog';
|
||||
static const String noteDialogTag = 'showRoomRocketNoteDialog';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<RtcProvider, SCRoomRocketStatusRes?>(
|
||||
selector: (_, provider) => provider.roomRocketStatus,
|
||||
builder: (context, status, child) {
|
||||
final level = _currentEntryLevel(status);
|
||||
final percent = _entryPercent(status);
|
||||
final shouldShake =
|
||||
(status?.shake ?? false) ||
|
||||
(level != null && percent > level.shakeThresholdPercent);
|
||||
return SCDebounceWidget(
|
||||
onTap: () => _showRocketDialog(context, status),
|
||||
child: _RoomRocketEntryButton(
|
||||
iconUrl: level?.rocketIconUrl ?? '',
|
||||
progressUrl: level?.progressBarUrl ?? '',
|
||||
shake: shouldShake,
|
||||
loading: status == null,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
SCRoomRocketLevelRes? _currentEntryLevel(SCRoomRocketStatusRes? status) {
|
||||
final levels = status?.levels ?? const <SCRoomRocketLevelRes>[];
|
||||
if (levels.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final currentLevel = (status?.currentLevel ?? status?.level?.toInt() ?? 1)
|
||||
.clamp(1, 99);
|
||||
for (final item in levels) {
|
||||
if (item.level == currentLevel) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
final sorted = [...levels]..sort((a, b) {
|
||||
final sortCompare = a.sort.compareTo(b.sort);
|
||||
return sortCompare != 0 ? sortCompare : a.level.compareTo(b.level);
|
||||
});
|
||||
return sorted.first;
|
||||
}
|
||||
|
||||
double _entryPercent(SCRoomRocketStatusRes? status) {
|
||||
final value = status?.displayPercent ?? status?.energyPercent;
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
final raw = value.toDouble();
|
||||
final percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
|
||||
return percent.clamp(0, 100).toDouble();
|
||||
}
|
||||
|
||||
void _showRocketDialog(BuildContext context, SCRoomRocketStatusRes? status) {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final roomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
|
||||
final navigator = Navigator.of(context, rootNavigator: true);
|
||||
_rocketUiDebug(
|
||||
'tap rocket entry roomId=$roomId hasStatus=${status != null}',
|
||||
);
|
||||
SmartDialog.show(
|
||||
tag: dialogTag,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => _RoomRocketDialogLoader(
|
||||
roomId: roomId,
|
||||
navigator: navigator,
|
||||
initialStatus: status,
|
||||
onClose: () => SmartDialog.dismiss(tag: dialogTag),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketEntryButton extends StatefulWidget {
|
||||
const _RoomRocketEntryButton({
|
||||
required this.iconUrl,
|
||||
required this.progressUrl,
|
||||
required this.shake,
|
||||
required this.loading,
|
||||
});
|
||||
|
||||
final String iconUrl;
|
||||
final String progressUrl;
|
||||
final bool shake;
|
||||
final bool loading;
|
||||
|
||||
@override
|
||||
State<_RoomRocketEntryButton> createState() => _RoomRocketEntryButtonState();
|
||||
}
|
||||
|
||||
class _RoomRocketEntryButtonState extends State<_RoomRocketEntryButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _shakeController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_shakeController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 480),
|
||||
);
|
||||
_syncShake();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _RoomRocketEntryButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.shake != widget.shake) {
|
||||
_syncShake();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shakeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _syncShake() {
|
||||
if (widget.shake) {
|
||||
_shakeController.repeat();
|
||||
return;
|
||||
}
|
||||
_shakeController
|
||||
..stop()
|
||||
..value = 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _shakeController,
|
||||
builder: (context, child) {
|
||||
final wave = math.sin(_shakeController.value * math.pi * 2);
|
||||
return Transform.translate(
|
||||
offset: widget.shake ? Offset(wave * 1.4.w, 0) : Offset.zero,
|
||||
child: Transform.rotate(
|
||||
angle: widget.shake ? wave * 0.065 : 0,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 46.w,
|
||||
height: 56.w,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
child: _RocketEntryImage(
|
||||
source: widget.iconUrl,
|
||||
loading: widget.loading,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
SizedBox(
|
||||
width: 42.w,
|
||||
height: 8.w,
|
||||
child:
|
||||
widget.progressUrl.trim().isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5.w),
|
||||
child: _RocketEntryImage(
|
||||
source: widget.progressUrl,
|
||||
loading: false,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RocketEntryImage extends StatelessWidget {
|
||||
const _RocketEntryImage({
|
||||
required this.source,
|
||||
this.loading = false,
|
||||
this.fit = BoxFit.contain,
|
||||
});
|
||||
|
||||
final String source;
|
||||
final bool loading;
|
||||
final BoxFit fit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = source.trim();
|
||||
if (value.isEmpty) {
|
||||
return _placeholder();
|
||||
}
|
||||
if (RoomRocketPagEffectOverlay.isPag(value)) {
|
||||
return RoomRocketPagPreview(
|
||||
resource: value,
|
||||
loop: true,
|
||||
defaultBuilder: (_) => _placeholder(),
|
||||
);
|
||||
}
|
||||
if (value.startsWith('sc_images/')) {
|
||||
return Image.asset(value, fit: fit, gaplessPlayback: true);
|
||||
}
|
||||
if (_isInlineSvg(value)) {
|
||||
return SvgPicture.string(
|
||||
value,
|
||||
fit: fit,
|
||||
errorBuilder: (_, __, ___) => _placeholder(),
|
||||
);
|
||||
}
|
||||
if (_isSvg(value)) {
|
||||
return SvgPicture.network(
|
||||
value,
|
||||
fit: fit,
|
||||
errorBuilder: (_, __, ___) => _placeholder(),
|
||||
);
|
||||
}
|
||||
return CustomCachedImage(
|
||||
imageUrl: value,
|
||||
fit: fit,
|
||||
placeholder: _placeholder(),
|
||||
errorWidget: const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
static bool _isSvg(String value) {
|
||||
final lower = value.toLowerCase();
|
||||
return lower.endsWith('.svg') || lower.contains('.svg?');
|
||||
}
|
||||
|
||||
static bool _isInlineSvg(String value) {
|
||||
final trimmed = value.trimLeft();
|
||||
return trimmed.startsWith('<svg') ||
|
||||
(trimmed.startsWith('<?xml') && trimmed.contains('<svg'));
|
||||
}
|
||||
|
||||
Widget _placeholder() {
|
||||
if (!loading) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return const Center(child: SCRotatingDotsLoading());
|
||||
}
|
||||
}
|
||||
|
||||
const int _rocketPageSize = 10;
|
||||
|
||||
class _RoomRocketDialogLoader extends StatefulWidget {
|
||||
const _RoomRocketDialogLoader({
|
||||
required this.roomId,
|
||||
required this.navigator,
|
||||
this.initialStatus,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final String roomId;
|
||||
final NavigatorState navigator;
|
||||
final SCRoomRocketStatusRes? initialStatus;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
State<_RoomRocketDialogLoader> createState() =>
|
||||
_RoomRocketDialogLoaderState();
|
||||
}
|
||||
|
||||
class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
SCRoomRocketStatusRes? _status;
|
||||
List<RoomRocketNoteSection>? _noteSections;
|
||||
int _selectedLevel = 0;
|
||||
RoomRocketRewardTab _selectedRewardTab = RoomRocketRewardTab.top1;
|
||||
bool _levelTouched = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_status = widget.initialStatus;
|
||||
_selectedLevel = _levelIndexFromStatus(widget.initialStatus);
|
||||
unawaited(_load());
|
||||
unawaited(_loadRule());
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
if (widget.roomId.trim().isEmpty) {
|
||||
_rocketUiDebug('skip status request because roomId is empty');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_rocketUiDebug('request status from main dialog roomId=${widget.roomId}');
|
||||
final res = await SCChatRoomRepository().rocketStatus(widget.roomId);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_debugStatusAssets(res);
|
||||
setState(() {
|
||||
_status = res;
|
||||
if (!_levelTouched) {
|
||||
_selectedLevel = _levelIndexFromStatus(res);
|
||||
}
|
||||
});
|
||||
try {
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).updateRoomRocketConfigurationStatus(res);
|
||||
} catch (_) {}
|
||||
} catch (error) {
|
||||
_rocketUiDebug('status request failed: $error');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadRule() async {
|
||||
try {
|
||||
_rocketUiDebug('prefetch rule from main dialog');
|
||||
final rule = await SCChatRoomRepository().roomRocketRule();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_noteSections = RoomRocketApiMapper.noteSectionsFromRule(rule);
|
||||
});
|
||||
} catch (error) {
|
||||
_rocketUiDebug('prefetch rule failed: $error');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final status = _status;
|
||||
final selectedLevel = _selectedLevel.clamp(0, 5).toInt();
|
||||
return RoomRocketDialog(
|
||||
status: status,
|
||||
levels: RoomRocketApiMapper.levelsFromStatus(status),
|
||||
selectedLevel: selectedLevel,
|
||||
onLevelSelected: _onLevelSelected,
|
||||
rewards: RoomRocketApiMapper.rewardsFromStatus(status),
|
||||
rewardGroups: RoomRocketApiMapper.rewardGroupsFromStatus(status),
|
||||
selectedRewardTab: _selectedRewardTab,
|
||||
onRewardTabSelected: _onRewardTabSelected,
|
||||
crew: RoomRocketApiMapper.crewFromStatus(status),
|
||||
onClose: widget.onClose,
|
||||
onOpenRank: () => _openRank(selectedLevel),
|
||||
onOpenRecord: _openRecord,
|
||||
onOpenNote: _openNote,
|
||||
);
|
||||
}
|
||||
|
||||
int _levelIndexFromStatus(SCRoomRocketStatusRes? status) {
|
||||
return ((status?.currentLevel ?? status?.level ?? 1) - 1)
|
||||
.clamp(0, 5)
|
||||
.toInt();
|
||||
}
|
||||
|
||||
void _debugStatusAssets(SCRoomRocketStatusRes status) {
|
||||
final levels = status.levels ?? const <SCRoomRocketLevelRes>[];
|
||||
_rocketUiDebug(
|
||||
'status assets loaded roomId=${status.roomId} levels=${levels.length} '
|
||||
'currentLevel=${status.currentLevel} selectedIndex=${_levelIndexFromStatus(status)}',
|
||||
);
|
||||
for (final item in levels) {
|
||||
_rocketUiDebug(
|
||||
'level=${item.level} icon=${_shortRocketDebug(item.rocketIconUrl)} '
|
||||
'centerPreview=${_shortRocketDebug(item.rocketPreviewUrl)} '
|
||||
'animation=${_shortRocketDebug(item.rocketAnimationUrl)} '
|
||||
'progress=${_shortRocketDebug(item.progressBarUrl)} '
|
||||
'shakeThreshold=${item.shakeThresholdPercent}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onLevelSelected(int index) {
|
||||
_rocketUiDebug('select rocket level=${index + 1}');
|
||||
setState(() {
|
||||
_levelTouched = true;
|
||||
_selectedLevel = index.clamp(0, 5).toInt();
|
||||
});
|
||||
}
|
||||
|
||||
void _onRewardTabSelected(RoomRocketRewardTab tab) {
|
||||
if (_selectedRewardTab == tab) {
|
||||
return;
|
||||
}
|
||||
_rocketUiDebug('select reward tab=${tab.name}');
|
||||
setState(() => _selectedRewardTab = tab);
|
||||
}
|
||||
|
||||
void _openRank(int selectedLevel) {
|
||||
_rocketUiDebug(
|
||||
'tap rocket king roomId=${widget.roomId} level=${selectedLevel + 1}',
|
||||
);
|
||||
SmartDialog.dismiss(tag: RoomRocketFloatingEntry.dialogTag).then((_) {
|
||||
if (!widget.navigator.mounted) {
|
||||
_rocketUiDebug('skip rocket king push because navigator is unmounted');
|
||||
return;
|
||||
}
|
||||
_rocketUiDebug(
|
||||
'push rocket king roomId=${widget.roomId} level=${selectedLevel + 1}',
|
||||
);
|
||||
VoiceRoomRoute.openRoomRocketRankOn(
|
||||
widget.navigator,
|
||||
roomId: widget.roomId,
|
||||
initialLevel: selectedLevel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _openRecord() {
|
||||
_rocketUiDebug('tap record roomId=${widget.roomId}');
|
||||
SmartDialog.dismiss(tag: RoomRocketFloatingEntry.dialogTag).then((_) {
|
||||
SmartDialog.show(
|
||||
tag: RoomRocketFloatingEntry.recordDialogTag,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => _RoomRocketRecordDialogLoader(
|
||||
roomId: widget.roomId,
|
||||
onClose:
|
||||
() => SmartDialog.dismiss(
|
||||
tag: RoomRocketFloatingEntry.recordDialogTag,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _openNote() {
|
||||
_rocketUiDebug('tap note roomId=${widget.roomId}');
|
||||
SmartDialog.dismiss(tag: RoomRocketFloatingEntry.dialogTag).then((_) {
|
||||
SmartDialog.show(
|
||||
tag: RoomRocketFloatingEntry.noteDialogTag,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => _RoomRocketNoteDialogLoader(
|
||||
initialSections: _noteSections,
|
||||
onClose:
|
||||
() => SmartDialog.dismiss(
|
||||
tag: RoomRocketFloatingEntry.noteDialogTag,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketRecordDialogLoader extends StatefulWidget {
|
||||
const _RoomRocketRecordDialogLoader({required this.roomId, this.onClose});
|
||||
|
||||
final String roomId;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
State<_RoomRocketRecordDialogLoader> createState() =>
|
||||
_RoomRocketRecordDialogLoaderState();
|
||||
}
|
||||
|
||||
class _RoomRocketRecordDialogLoaderState
|
||||
extends State<_RoomRocketRecordDialogLoader> {
|
||||
final List<SCRoomRocketRewardRecordRes> _rawRecords = [];
|
||||
List<RoomRocketRecordItem>? _records;
|
||||
bool _loading = true;
|
||||
bool _loadingMore = false;
|
||||
bool _hasMore = true;
|
||||
int _cursor = 1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(_load(reset: true));
|
||||
}
|
||||
|
||||
Future<void> _load({bool reset = false}) async {
|
||||
if (_loadingMore || (!reset && (!_hasMore || _loading))) {
|
||||
return;
|
||||
}
|
||||
final requestCursor = reset ? 1 : _cursor;
|
||||
setState(() {
|
||||
if (reset) {
|
||||
_loading = true;
|
||||
} else {
|
||||
_loadingMore = true;
|
||||
}
|
||||
});
|
||||
try {
|
||||
_rocketUiDebug(
|
||||
'request reward-records roomId=${widget.roomId} '
|
||||
'cursor=$requestCursor limit=$_rocketPageSize',
|
||||
);
|
||||
final res = await SCChatRoomRepository().roomRocketRewardRecords(
|
||||
widget.roomId,
|
||||
cursor: requestCursor,
|
||||
limit: _rocketPageSize,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final nextRecords = res.records;
|
||||
setState(() {
|
||||
if (reset) {
|
||||
_rawRecords
|
||||
..clear()
|
||||
..addAll(nextRecords);
|
||||
} else {
|
||||
_rawRecords.addAll(nextRecords);
|
||||
}
|
||||
_records = RoomRocketApiMapper.recordItemsFromRecords(_rawRecords);
|
||||
_hasMore = nextRecords.length >= _rocketPageSize;
|
||||
if (_hasMore) {
|
||||
_cursor = requestCursor + 1;
|
||||
}
|
||||
_loading = false;
|
||||
_loadingMore = false;
|
||||
});
|
||||
_rocketUiDebug(
|
||||
'reward-records loaded cursor=$requestCursor '
|
||||
'count=${nextRecords.length} total=${_rawRecords.length} '
|
||||
'hasMore=$_hasMore nextCursor=$_cursor',
|
||||
);
|
||||
} catch (error) {
|
||||
_rocketUiDebug('reward-records request failed: $error');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_loadingMore = false;
|
||||
_hasMore = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoomRocketRecordDialog(
|
||||
records: _records,
|
||||
useMockFallback: false,
|
||||
loadingText: _loading ? 'Loading data......' : '',
|
||||
isLoadingMore: _loadingMore,
|
||||
hasMore: _hasMore,
|
||||
onLoadMore: () => unawaited(_load()),
|
||||
limitText: 'Show only records from the last 35 days',
|
||||
onClose: widget.onClose,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketNoteDialogLoader extends StatefulWidget {
|
||||
const _RoomRocketNoteDialogLoader({this.initialSections, this.onClose});
|
||||
|
||||
final List<RoomRocketNoteSection>? initialSections;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
State<_RoomRocketNoteDialogLoader> createState() =>
|
||||
_RoomRocketNoteDialogLoaderState();
|
||||
}
|
||||
|
||||
class _RoomRocketNoteDialogLoaderState
|
||||
extends State<_RoomRocketNoteDialogLoader> {
|
||||
List<RoomRocketNoteSection>? _sections;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sections = widget.initialSections;
|
||||
if (_sections != null) {
|
||||
return;
|
||||
}
|
||||
unawaited(_load());
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
_rocketUiDebug('request rule');
|
||||
final rule = await SCChatRoomRepository().roomRocketRule();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_sections = RoomRocketApiMapper.noteSectionsFromRule(rule);
|
||||
});
|
||||
} catch (error) {
|
||||
_rocketUiDebug('rule request failed: $error');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoomRocketNoteDialog(sections: _sections, onClose: widget.onClose);
|
||||
}
|
||||
}
|
||||
|
||||
void _rocketUiDebug(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[RoomRocketUi] $message');
|
||||
}
|
||||
}
|
||||
|
||||
String _shortRocketDebug(String value) {
|
||||
final text = value.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
if (text.isEmpty) {
|
||||
return '-';
|
||||
}
|
||||
if (text.length <= 140) {
|
||||
return text;
|
||||
}
|
||||
return '${text.substring(0, 137)}...';
|
||||
}
|
||||
389
lib/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart
Normal file
@ -0,0 +1,389 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
|
||||
class RoomRocketNoteDialog extends StatelessWidget {
|
||||
const RoomRocketNoteDialog({super.key, this.sections, this.onClose});
|
||||
|
||||
final List<RoomRocketNoteSection>? sections;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context);
|
||||
final title = _translate(localizations, 'roomRocketNoteTitle', 'Note');
|
||||
final resolvedSections = _normalizeSections(localizations, sections);
|
||||
final contentDirection = Directionality.of(context);
|
||||
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onClose,
|
||||
child: Container(color: Colors.black.withValues(alpha: 0.60)),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final scale = math.min(
|
||||
constraints.maxWidth / _designWidth,
|
||||
constraints.maxHeight / _designHeight,
|
||||
);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
width: _designWidth * scale,
|
||||
height: _designHeight * scale,
|
||||
child: _NoteDialogCanvas(
|
||||
scale: scale,
|
||||
title: title,
|
||||
sections: resolvedSections,
|
||||
contentDirection: contentDirection,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<RoomRocketNoteSection> _normalizeSections(
|
||||
SCAppLocalizations? localizations,
|
||||
List<RoomRocketNoteSection>? sections,
|
||||
) {
|
||||
final source =
|
||||
sections
|
||||
?.where(
|
||||
(item) =>
|
||||
item.title.trim().isNotEmpty || item.body.trim().isNotEmpty,
|
||||
)
|
||||
.toList(growable: false) ??
|
||||
const <RoomRocketNoteSection>[];
|
||||
if (source.isNotEmpty) {
|
||||
return source;
|
||||
}
|
||||
final title = _translate(
|
||||
localizations,
|
||||
'roomRocketNoteSectionTitle',
|
||||
'Each time a VIP',
|
||||
);
|
||||
final body = _translate(
|
||||
localizations,
|
||||
'roomRocketNoteSectionBody',
|
||||
'1. Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration.',
|
||||
);
|
||||
return List<RoomRocketNoteSection>.generate(
|
||||
5,
|
||||
(_) => RoomRocketNoteSection(title: title, body: body),
|
||||
growable: false,
|
||||
);
|
||||
}
|
||||
|
||||
static String _translate(
|
||||
SCAppLocalizations? localizations,
|
||||
String key,
|
||||
String fallback,
|
||||
) {
|
||||
final value = localizations?.translate(key);
|
||||
if (value == null || value == key) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomRocketNoteSection {
|
||||
const RoomRocketNoteSection({required this.title, required this.body});
|
||||
|
||||
final String title;
|
||||
final String body;
|
||||
}
|
||||
|
||||
class _NoteDialogCanvas extends StatelessWidget {
|
||||
const _NoteDialogCanvas({
|
||||
required this.scale,
|
||||
required this.title,
|
||||
required this.sections,
|
||||
required this.contentDirection,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final double scale;
|
||||
final String title;
|
||||
final List<RoomRocketNoteSection> sections;
|
||||
final TextDirection contentDirection;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
double s(double value) => value * scale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
_buildPanelBackground(),
|
||||
_buildHeader(),
|
||||
_buildContent(),
|
||||
_buildCloseButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPanelBackground() {
|
||||
return Stack(
|
||||
children: [
|
||||
_image(_NoteDialogAssets.panelTop, 8, 176, 360, 63),
|
||||
for (final top in const [
|
||||
239.0,
|
||||
298.0,
|
||||
357.0,
|
||||
416.0,
|
||||
475.0,
|
||||
534.0,
|
||||
593.0,
|
||||
652.0,
|
||||
])
|
||||
_image(_NoteDialogAssets.panelMiddle, 8, top, 360, 59),
|
||||
_image(_NoteDialogAssets.panelBottom, 8, 673, 360, 76),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: s(196),
|
||||
width: s(_designWidth),
|
||||
child: _GradientText(
|
||||
title,
|
||||
fontSize: s(16),
|
||||
fontWeight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: s(28),
|
||||
top: s(220),
|
||||
width: s(320),
|
||||
height: math.max(1, s(1)),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFB2FFE7).withValues(alpha: 0.16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
final isRtl = contentDirection == TextDirection.rtl;
|
||||
return Positioned(
|
||||
left: s(35),
|
||||
top: s(254),
|
||||
width: s(305),
|
||||
height: s(448),
|
||||
child: Directionality(
|
||||
textDirection: contentDirection,
|
||||
child: ScrollConfiguration(
|
||||
behavior: const _NoGlowScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
primary: false,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: sections.length,
|
||||
separatorBuilder: (_, __) => SizedBox(height: s(12)),
|
||||
itemBuilder: (context, index) {
|
||||
final item = sections[index];
|
||||
return _NoteSectionView(
|
||||
scale: scale,
|
||||
section: item,
|
||||
textAlign: isRtl ? TextAlign.right : TextAlign.left,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCloseButton() {
|
||||
return Positioned(
|
||||
left: s(326),
|
||||
top: s(182),
|
||||
width: s(32),
|
||||
height: s(32),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onClose,
|
||||
child: Center(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: const Color(0xFF75F3CB).withValues(alpha: 0.72),
|
||||
width: s(1),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: s(18),
|
||||
height: s(18),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: s(15),
|
||||
color: const Color(0xFF75F3CB).withValues(alpha: 0.86),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _image(
|
||||
String asset,
|
||||
double left,
|
||||
double top,
|
||||
double width,
|
||||
double height, {
|
||||
BoxFit fit = BoxFit.fill,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: s(left),
|
||||
top: s(top),
|
||||
width: s(width),
|
||||
height: s(height),
|
||||
child: Image.asset(asset, fit: fit, gaplessPlayback: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoteSectionView extends StatelessWidget {
|
||||
const _NoteSectionView({
|
||||
required this.scale,
|
||||
required this.section,
|
||||
required this.textAlign,
|
||||
});
|
||||
|
||||
final double scale;
|
||||
final RoomRocketNoteSection section;
|
||||
final TextAlign textAlign;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
textAlign == TextAlign.right
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
section.title,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: textAlign,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF75F3CB),
|
||||
fontSize: 16 * scale,
|
||||
height: 18 / 16,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8 * scale),
|
||||
Text(
|
||||
section.body,
|
||||
textScaler: TextScaler.noScaling,
|
||||
textAlign: textAlign,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16 * scale,
|
||||
height: 18 / 16,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GradientText extends StatelessWidget {
|
||||
const _GradientText(
|
||||
this.text, {
|
||||
required this.fontSize,
|
||||
this.fontWeight = FontWeight.w400,
|
||||
this.textAlign = TextAlign.left,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final double fontSize;
|
||||
final FontWeight fontWeight;
|
||||
final TextAlign textAlign;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShaderMask(
|
||||
shaderCallback:
|
||||
(bounds) => LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: const [Colors.white, Color(0xFFA4FFE3)],
|
||||
).createShader(bounds),
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Text(
|
||||
text,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
textAlign: textAlign,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
height: 1,
|
||||
fontWeight: fontWeight,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoGlowScrollBehavior extends ScrollBehavior {
|
||||
const _NoGlowScrollBehavior();
|
||||
|
||||
@override
|
||||
Widget buildOverscrollIndicator(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
ScrollableDetails details,
|
||||
) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _NoteDialogAssets {
|
||||
const _NoteDialogAssets._();
|
||||
|
||||
static const String panelTop = 'sc_images/room/rocket/reward_dialog_top.png';
|
||||
static const String panelMiddle =
|
||||
'sc_images/room/rocket/reward_dialog_middle.png';
|
||||
static const String panelBottom =
|
||||
'sc_images/room/rocket/reward_dialog_bottom.png';
|
||||
}
|
||||
|
||||
const double _designWidth = 375;
|
||||
const double _designHeight = 812;
|
||||
@ -0,0 +1,243 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pag/pag.dart';
|
||||
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||
|
||||
class RoomRocketPagEffectOverlay extends StatefulWidget {
|
||||
const RoomRocketPagEffectOverlay({
|
||||
super.key,
|
||||
required this.resource,
|
||||
this.topAvatarUrl,
|
||||
this.onCompleted,
|
||||
});
|
||||
|
||||
final String resource;
|
||||
final String? topAvatarUrl;
|
||||
final VoidCallback? onCompleted;
|
||||
|
||||
static bool isPag(String? resource) {
|
||||
final lower = resource?.trim().toLowerCase() ?? '';
|
||||
return lower.contains('.pag');
|
||||
}
|
||||
|
||||
@override
|
||||
State<RoomRocketPagEffectOverlay> createState() =>
|
||||
_RoomRocketPagEffectOverlayState();
|
||||
}
|
||||
|
||||
class _RoomRocketPagEffectOverlayState
|
||||
extends State<RoomRocketPagEffectOverlay> {
|
||||
bool _completed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final resource = widget.resource.trim();
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
return IgnorePointer(
|
||||
child: ColoredBox(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_RoomRocketPagTopAvatar(
|
||||
avatarUrl: widget.topAvatarUrl,
|
||||
canvasSize: size,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: _RoomRocketPagView(
|
||||
resource: resource,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
loop: false,
|
||||
onCompleted: _complete,
|
||||
defaultBuilder:
|
||||
(_) => const Center(child: SCRotatingDotsLoading()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _complete() {
|
||||
if (_completed) {
|
||||
return;
|
||||
}
|
||||
_completed = true;
|
||||
widget.onCompleted?.call();
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketPagTopAvatar extends StatelessWidget {
|
||||
const _RoomRocketPagTopAvatar({
|
||||
required this.avatarUrl,
|
||||
required this.canvasSize,
|
||||
});
|
||||
|
||||
final String? avatarUrl;
|
||||
final Size canvasSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final url = avatarUrl?.trim() ?? '';
|
||||
if (url.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final size = (canvasSize.width * 0.15).clamp(46.0, 66.0).toDouble();
|
||||
return Positioned(
|
||||
left: (canvasSize.width - size) / 2,
|
||||
top: canvasSize.height * 0.098,
|
||||
width: size,
|
||||
height: size,
|
||||
child: ClipOval(
|
||||
child: CustomCachedImage(
|
||||
imageUrl: url,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: const SizedBox.shrink(),
|
||||
errorWidget: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomRocketPagPreview extends StatelessWidget {
|
||||
const RoomRocketPagPreview({
|
||||
super.key,
|
||||
required this.resource,
|
||||
this.width,
|
||||
this.height,
|
||||
this.loop = true,
|
||||
this.defaultBuilder,
|
||||
});
|
||||
|
||||
final String resource;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final bool loop;
|
||||
final WidgetBuilder? defaultBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final resolvedWidth =
|
||||
width ??
|
||||
(constraints.maxWidth.isFinite ? constraints.maxWidth : null);
|
||||
final resolvedHeight =
|
||||
height ??
|
||||
(constraints.maxHeight.isFinite ? constraints.maxHeight : null);
|
||||
return _RoomRocketPagView(
|
||||
resource: resource,
|
||||
width: resolvedWidth,
|
||||
height: resolvedHeight,
|
||||
loop: loop,
|
||||
defaultBuilder:
|
||||
defaultBuilder ??
|
||||
(_) => const Center(child: SCRotatingDotsLoading()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRocketPagView extends StatefulWidget {
|
||||
const _RoomRocketPagView({
|
||||
required this.resource,
|
||||
this.width,
|
||||
this.height,
|
||||
required this.loop,
|
||||
this.onCompleted,
|
||||
this.defaultBuilder,
|
||||
});
|
||||
|
||||
final String resource;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final bool loop;
|
||||
final VoidCallback? onCompleted;
|
||||
final WidgetBuilder? defaultBuilder;
|
||||
|
||||
@override
|
||||
State<_RoomRocketPagView> createState() => _RoomRocketPagViewState();
|
||||
}
|
||||
|
||||
class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
late Future<Uint8List?> _bytesFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bytesFuture = _loadPagBytes(widget.resource.trim());
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _RoomRocketPagView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.resource.trim() != widget.resource.trim()) {
|
||||
_bytesFuture = _loadPagBytes(widget.resource.trim());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final path = widget.resource.trim();
|
||||
if (path.isEmpty || !RoomRocketPagEffectOverlay.isPag(path)) {
|
||||
return widget.defaultBuilder?.call(context) ?? const SizedBox.shrink();
|
||||
}
|
||||
final repeatCount =
|
||||
widget.loop ? PAGView.REPEAT_COUNT_LOOP : PAGView.REPEAT_COUNT_DEFAULT;
|
||||
final defaultChild =
|
||||
widget.defaultBuilder?.call(context) ??
|
||||
const Center(child: SCRotatingDotsLoading());
|
||||
return FutureBuilder<Uint8List?>(
|
||||
future: _bytesFuture,
|
||||
builder: (context, snapshot) {
|
||||
final bytes = snapshot.data;
|
||||
if (snapshot.connectionState != ConnectionState.done ||
|
||||
bytes == null ||
|
||||
bytes.isEmpty) {
|
||||
return defaultChild;
|
||||
}
|
||||
return PAGView.bytes(
|
||||
bytes,
|
||||
key: ValueKey('room-rocket-pag:$path:${widget.loop}'),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
autoPlay: true,
|
||||
repeatCount: repeatCount,
|
||||
onAnimationEnd: widget.onCompleted,
|
||||
defaultBuilder: (_) => defaultChild,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List?> _loadPagBytes(String path) async {
|
||||
try {
|
||||
final data =
|
||||
_isNetwork(path)
|
||||
? await NetworkAssetBundle(Uri.parse(path)).load(path)
|
||||
: await rootBundle.load(path);
|
||||
return data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
||||
} catch (error) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[RoomRocketPag] load failed path=$path error=$error');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isNetwork(String value) {
|
||||
return value.startsWith('http://') || value.startsWith('https://');
|
||||
}
|
||||
}
|
||||
814
lib/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart
Normal file
@ -0,0 +1,814 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
|
||||
|
||||
class RoomRocketRecordDialog extends StatelessWidget {
|
||||
const RoomRocketRecordDialog({
|
||||
super.key,
|
||||
this.records,
|
||||
this.loadingText = 'Loading data......',
|
||||
this.limitText = 'Show only records from the last 35 days',
|
||||
this.isLoadingMore = false,
|
||||
this.hasMore = false,
|
||||
this.onLoadMore,
|
||||
this.useMockFallback = true,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final List<RoomRocketRecordItem>? records;
|
||||
final String loadingText;
|
||||
final String limitText;
|
||||
final bool isLoadingMore;
|
||||
final bool hasMore;
|
||||
final VoidCallback? onLoadMore;
|
||||
final bool useMockFallback;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context);
|
||||
final emptyText = _translate(
|
||||
localizations,
|
||||
'roomRocketRecordEmptyText',
|
||||
'No reward records',
|
||||
);
|
||||
final contentDirection = Directionality.of(context);
|
||||
final resolvedRecords = _normalizeRecords(
|
||||
records,
|
||||
useMockFallback: useMockFallback,
|
||||
);
|
||||
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onClose,
|
||||
child: Container(color: Colors.black.withValues(alpha: 0.60)),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final scale = math.min(
|
||||
constraints.maxWidth / _designWidth,
|
||||
constraints.maxHeight / _designHeight,
|
||||
);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
width: _designWidth * scale,
|
||||
height: _designHeight * scale,
|
||||
child: _RecordDialogCanvas(
|
||||
scale: scale,
|
||||
records: resolvedRecords,
|
||||
loadingText: loadingText,
|
||||
limitText: limitText,
|
||||
isLoadingMore: isLoadingMore,
|
||||
hasMore: hasMore,
|
||||
onLoadMore: onLoadMore,
|
||||
emptyText: emptyText,
|
||||
contentDirection: contentDirection,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<RoomRocketRecordItem> _normalizeRecords(
|
||||
List<RoomRocketRecordItem>? records, {
|
||||
required bool useMockFallback,
|
||||
}) {
|
||||
final source =
|
||||
records
|
||||
?.where((item) => item.roomId.trim().isNotEmpty)
|
||||
.toList(growable: false) ??
|
||||
const <RoomRocketRecordItem>[];
|
||||
if (source.isNotEmpty) {
|
||||
return source;
|
||||
}
|
||||
if (!useMockFallback) {
|
||||
return const <RoomRocketRecordItem>[];
|
||||
}
|
||||
const reward = RoomRocketRewardItem(
|
||||
asset: _RecordDialogAssets.rewardThumb,
|
||||
amount: '13.2k',
|
||||
);
|
||||
return List<RoomRocketRecordItem>.generate(
|
||||
7,
|
||||
(_) => const RoomRocketRecordItem(
|
||||
roomId: 'RoomID:1235645',
|
||||
levelLabel: 'Top1',
|
||||
time: '31/01 14:14',
|
||||
rewards: [reward, reward, reward, reward],
|
||||
),
|
||||
growable: false,
|
||||
);
|
||||
}
|
||||
|
||||
static String _translate(
|
||||
SCAppLocalizations? localizations,
|
||||
String key,
|
||||
String fallback,
|
||||
) {
|
||||
final value = localizations?.translate(key);
|
||||
if (value == null || value == key) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomRocketRecordItem {
|
||||
const RoomRocketRecordItem({
|
||||
required this.roomId,
|
||||
required this.levelLabel,
|
||||
required this.time,
|
||||
this.rewards = const <RoomRocketRewardItem>[],
|
||||
});
|
||||
|
||||
final String roomId;
|
||||
final String levelLabel;
|
||||
final String time;
|
||||
final List<RoomRocketRewardItem> rewards;
|
||||
}
|
||||
|
||||
class _RecordDialogCanvas extends StatelessWidget {
|
||||
const _RecordDialogCanvas({
|
||||
required this.scale,
|
||||
required this.records,
|
||||
required this.loadingText,
|
||||
required this.limitText,
|
||||
required this.isLoadingMore,
|
||||
required this.hasMore,
|
||||
this.onLoadMore,
|
||||
required this.emptyText,
|
||||
required this.contentDirection,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final double scale;
|
||||
final List<RoomRocketRecordItem> records;
|
||||
final String loadingText;
|
||||
final String limitText;
|
||||
final bool isLoadingMore;
|
||||
final bool hasMore;
|
||||
final VoidCallback? onLoadMore;
|
||||
final String emptyText;
|
||||
final TextDirection contentDirection;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
double s(double value) => value * scale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLoading = loadingText.trim().isNotEmpty;
|
||||
final isEmpty = !isLoading && records.isEmpty;
|
||||
return Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
_buildPanelBackground(),
|
||||
_buildHeader(),
|
||||
_buildColumnTitles(),
|
||||
if (isEmpty) _buildEmptyState() else _buildRecordList(),
|
||||
_buildFooter(),
|
||||
_buildCloseButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPanelBackground() {
|
||||
return Stack(
|
||||
children: [
|
||||
_image(_RecordDialogAssets.panelTop, 8, 176, 360, 63),
|
||||
for (final top in const [
|
||||
239.0,
|
||||
298.0,
|
||||
357.0,
|
||||
416.0,
|
||||
475.0,
|
||||
534.0,
|
||||
593.0,
|
||||
652.0,
|
||||
])
|
||||
_image(_RecordDialogAssets.panelMiddle, 8, top, 360, 59),
|
||||
_image(_RecordDialogAssets.panelBottom, 8, 673, 360, 76),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: s(196),
|
||||
width: s(_designWidth),
|
||||
child: _GradientText(
|
||||
'Record',
|
||||
fontSize: s(16),
|
||||
fontWeight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: s(28),
|
||||
top: s(220),
|
||||
width: s(320),
|
||||
height: math.max(1, s(1)),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFB2FFE7).withValues(alpha: 0.16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColumnTitles() {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: s(66),
|
||||
top: s(240),
|
||||
width: s(79),
|
||||
height: s(14),
|
||||
child: _SectionTitle(text: 'Room', scale: scale),
|
||||
),
|
||||
Positioned(
|
||||
left: s(219),
|
||||
top: s(240),
|
||||
width: s(90),
|
||||
height: s(14),
|
||||
child: _SectionTitle(text: 'Reward', scale: scale),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordList() {
|
||||
return Positioned(
|
||||
left: s(11),
|
||||
top: s(268),
|
||||
width: s(353),
|
||||
height: s(419),
|
||||
child: ClipRect(
|
||||
child: ScrollConfiguration(
|
||||
behavior: const _NoGlowScrollBehavior(),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
primary: false,
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
itemExtent: s(61),
|
||||
itemCount: records.length + (isLoadingMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= records.length) {
|
||||
return _LoadMoreRow(scale: scale);
|
||||
}
|
||||
return _RecordRow(scale: scale, item: records[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (notification.metrics.axis != Axis.vertical) {
|
||||
return false;
|
||||
}
|
||||
if (!hasMore || isLoadingMore) {
|
||||
return false;
|
||||
}
|
||||
if (notification.metrics.extentAfter <= s(96)) {
|
||||
onLoadMore?.call();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Positioned(
|
||||
left: s(0),
|
||||
top: s(340),
|
||||
width: s(_designWidth),
|
||||
height: s(190),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
_RecordDialogAssets.empty,
|
||||
width: s(86),
|
||||
height: s(86),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: s(12)),
|
||||
SizedBox(
|
||||
width: s(268),
|
||||
child: Text(
|
||||
emptyText,
|
||||
textDirection: contentDirection,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: s(14),
|
||||
height: 1.25,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter() {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: s(703),
|
||||
width: s(_designWidth),
|
||||
child: _FooterText(
|
||||
text: loadingText,
|
||||
scale: scale,
|
||||
fontSize: 10,
|
||||
color: const Color(0xFF78FF78).withValues(alpha: 0.56),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: s(720),
|
||||
width: s(_designWidth),
|
||||
child: _FooterText(
|
||||
text: limitText,
|
||||
scale: scale,
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF78FF78),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCloseButton() {
|
||||
return Positioned(
|
||||
left: s(326),
|
||||
top: s(182),
|
||||
width: s(32),
|
||||
height: s(32),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onClose,
|
||||
child: Center(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: const Color(0xFF75F3CB).withValues(alpha: 0.72),
|
||||
width: s(1),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: s(18),
|
||||
height: s(18),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: s(15),
|
||||
color: const Color(0xFF75F3CB).withValues(alpha: 0.86),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _image(
|
||||
String asset,
|
||||
double left,
|
||||
double top,
|
||||
double width,
|
||||
double height, {
|
||||
BoxFit fit = BoxFit.fill,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: s(left),
|
||||
top: s(top),
|
||||
width: s(width),
|
||||
height: s(height),
|
||||
child: Image.asset(asset, fit: fit, gaplessPlayback: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordRow extends StatelessWidget {
|
||||
const _RecordRow({required this.scale, required this.item});
|
||||
|
||||
final double scale;
|
||||
final RoomRocketRecordItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rewards =
|
||||
item.rewards.isNotEmpty
|
||||
? item.rewards
|
||||
: const [
|
||||
RoomRocketRewardItem(
|
||||
asset: _RecordDialogAssets.rewardThumb,
|
||||
amount: '13.2k',
|
||||
),
|
||||
];
|
||||
return SizedBox(
|
||||
height: 61 * scale,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: math.max(1, scale),
|
||||
child: const _RowDivider(),
|
||||
),
|
||||
Positioned(
|
||||
left: 27 * scale,
|
||||
top: 0,
|
||||
width: 130 * scale,
|
||||
child: _PlainText(
|
||||
item.roomId,
|
||||
scale: scale,
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 27 * scale,
|
||||
top: 18 * scale,
|
||||
width: 90 * scale,
|
||||
child: _PlainText(
|
||||
item.levelLabel,
|
||||
scale: scale,
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 27 * scale,
|
||||
top: 34 * scale,
|
||||
width: 90 * scale,
|
||||
child: _PlainText(
|
||||
item.time,
|
||||
scale: scale,
|
||||
fontSize: 10,
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 179 * scale,
|
||||
top: 3 * scale,
|
||||
width: 166 * scale,
|
||||
height: 38 * scale,
|
||||
child: ClipRect(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var index = 0; index < rewards.length; index++) ...[
|
||||
_SmallReward(scale: scale, item: rewards[index]),
|
||||
if (index != rewards.length - 1)
|
||||
SizedBox(width: 12 * scale),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SmallReward extends StatelessWidget {
|
||||
const _SmallReward({required this.scale, required this.item});
|
||||
|
||||
final double scale;
|
||||
final RoomRocketRewardItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 28 * scale,
|
||||
height: 38 * scale,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 28 * scale,
|
||||
height: 28 * scale,
|
||||
child:
|
||||
item.isGoldReward
|
||||
? Image.asset(_RecordDialogAssets.coin, fit: BoxFit.contain)
|
||||
: _RocketImage(
|
||||
asset: item.asset ?? _RecordDialogAssets.rewardThumb,
|
||||
imageUrl: item.imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -3 * scale,
|
||||
top: 30 * scale,
|
||||
width: 34 * scale,
|
||||
child: Text(
|
||||
item.displayText,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8 * scale,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionTitle extends StatelessWidget {
|
||||
const _SectionTitle({required this.text, required this.scale});
|
||||
|
||||
final String text;
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_SlashGroup(scale: scale),
|
||||
SizedBox(width: 4 * scale),
|
||||
Text(
|
||||
text,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.visible,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF75F3CB),
|
||||
fontSize: 14 * scale,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4 * scale),
|
||||
Transform.rotate(angle: math.pi, child: _SlashGroup(scale: scale)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SlashGroup extends StatelessWidget {
|
||||
const _SlashGroup({required this.scale});
|
||||
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
'///',
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF75F3CB),
|
||||
fontSize: 10 * scale,
|
||||
height: 1,
|
||||
letterSpacing: 0,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlainText extends StatelessWidget {
|
||||
const _PlainText(
|
||||
this.text, {
|
||||
required this.scale,
|
||||
required this.fontSize,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final double scale;
|
||||
final double fontSize;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: fontSize * scale,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FooterText extends StatelessWidget {
|
||||
const _FooterText({
|
||||
required this.text,
|
||||
required this.scale,
|
||||
required this.fontSize,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final double scale;
|
||||
final double fontSize;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: fontSize * scale,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RocketImage extends StatelessWidget {
|
||||
const _RocketImage({this.asset, this.imageUrl, this.fit = BoxFit.cover});
|
||||
|
||||
final String? asset;
|
||||
final String? imageUrl;
|
||||
final BoxFit fit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (imageUrl != null && imageUrl!.trim().isNotEmpty) {
|
||||
return CustomCachedImage(imageUrl: imageUrl!, fit: fit);
|
||||
}
|
||||
if (asset != null && asset!.trim().isNotEmpty) {
|
||||
return Image.asset(asset!, fit: fit, gaplessPlayback: true);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class _GradientText extends StatelessWidget {
|
||||
const _GradientText(
|
||||
this.text, {
|
||||
required this.fontSize,
|
||||
this.fontWeight = FontWeight.w400,
|
||||
this.textAlign = TextAlign.left,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final double fontSize;
|
||||
final FontWeight fontWeight;
|
||||
final TextAlign textAlign;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShaderMask(
|
||||
shaderCallback:
|
||||
(bounds) => LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: const [Colors.white, Color(0xFFA4FFE3)],
|
||||
).createShader(bounds),
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Text(
|
||||
text,
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
textAlign: textAlign,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
height: 1,
|
||||
fontWeight: fontWeight,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RowDivider extends StatelessWidget {
|
||||
const _RowDivider();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.white.withValues(alpha: 0),
|
||||
Colors.white.withValues(alpha: 0.16),
|
||||
Colors.white.withValues(alpha: 0),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoGlowScrollBehavior extends ScrollBehavior {
|
||||
const _NoGlowScrollBehavior();
|
||||
|
||||
@override
|
||||
Widget buildOverscrollIndicator(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
ScrollableDetails details,
|
||||
) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _LoadMoreRow extends StatelessWidget {
|
||||
const _LoadMoreRow({required this.scale});
|
||||
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Loading data......',
|
||||
textScaler: TextScaler.noScaling,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF78FF78).withValues(alpha: 0.56),
|
||||
fontSize: 10 * scale,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordDialogAssets {
|
||||
const _RecordDialogAssets._();
|
||||
|
||||
static const String panelTop = 'sc_images/room/rocket/reward_dialog_top.png';
|
||||
static const String panelMiddle =
|
||||
'sc_images/room/rocket/reward_dialog_middle.png';
|
||||
static const String panelBottom =
|
||||
'sc_images/room/rocket/reward_dialog_bottom.png';
|
||||
static const String rewardThumb = 'sc_images/room/rocket/reward_thumb.png';
|
||||
static const String coin = 'sc_images/register_reward/yumi_coin.png';
|
||||
static const String empty = 'sc_images/room/sc_icon_room_music_empty.png';
|
||||
}
|
||||
|
||||
const double _designWidth = 375;
|
||||
const double _designHeight = 812;
|
||||
1110
lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart
Normal file
@ -0,0 +1,206 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart';
|
||||
|
||||
const int _rewardDialogPageSize = 10;
|
||||
|
||||
class RoomRocketRewardDialogLoader extends StatefulWidget {
|
||||
const RoomRocketRewardDialogLoader({
|
||||
super.key,
|
||||
required this.roomId,
|
||||
this.selectedReward,
|
||||
this.initialPopupRecords = const <SCRoomRocketRewardRecordRes>[],
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final String roomId;
|
||||
final RoomRocketRewardItem? selectedReward;
|
||||
final List<SCRoomRocketRewardRecordRes> initialPopupRecords;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
State<RoomRocketRewardDialogLoader> createState() =>
|
||||
_RoomRocketRewardDialogLoaderState();
|
||||
}
|
||||
|
||||
class _RoomRocketRewardDialogLoaderState
|
||||
extends State<RoomRocketRewardDialogLoader> {
|
||||
final List<SCRoomRocketRewardRecordRes> _popupRecords = [];
|
||||
final List<SCRoomRocketRewardRecordRes> _roomRecords = [];
|
||||
List<RoomRocketRewardItem>? _rewards;
|
||||
List<RoomRocketRewardWinner>? _winners;
|
||||
bool _loading = true;
|
||||
bool _loadingMore = false;
|
||||
bool _hasMore = true;
|
||||
bool _usedInitialPopupRecords = false;
|
||||
int _cursor = 1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.initialPopupRecords.isNotEmpty) {
|
||||
_popupRecords.addAll(widget.initialPopupRecords);
|
||||
_rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords);
|
||||
_winners = RoomRocketApiMapper.winnersFromRecords(_popupRecords);
|
||||
}
|
||||
unawaited(_load(reset: true));
|
||||
}
|
||||
|
||||
Future<void> _load({bool reset = false}) async {
|
||||
if (_loadingMore || (!reset && (!_hasMore || _loading))) {
|
||||
return;
|
||||
}
|
||||
final requestCursor = reset ? 1 : _cursor;
|
||||
setState(() {
|
||||
if (reset) {
|
||||
_loading = true;
|
||||
} else {
|
||||
_loadingMore = true;
|
||||
}
|
||||
});
|
||||
try {
|
||||
final useInitialPopupRecords =
|
||||
reset &&
|
||||
widget.initialPopupRecords.isNotEmpty &&
|
||||
!_usedInitialPopupRecords;
|
||||
final nextPopupRecords =
|
||||
useInitialPopupRecords
|
||||
? widget.initialPopupRecords
|
||||
: await _loadPopupRecords(requestCursor);
|
||||
if (useInitialPopupRecords) {
|
||||
_usedInitialPopupRecords = true;
|
||||
}
|
||||
final nextRoomRecords = await _loadRoomRewardRecords(requestCursor);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
if (reset) {
|
||||
_popupRecords
|
||||
..clear()
|
||||
..addAll(nextPopupRecords);
|
||||
_roomRecords
|
||||
..clear()
|
||||
..addAll(nextRoomRecords);
|
||||
} else {
|
||||
_popupRecords.addAll(nextPopupRecords);
|
||||
_roomRecords.addAll(nextRoomRecords);
|
||||
}
|
||||
_rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords);
|
||||
_winners = RoomRocketApiMapper.winnersFromRecords(
|
||||
_visibleRoomRewardRecords(),
|
||||
);
|
||||
_hasMore =
|
||||
nextPopupRecords.length >= _rewardDialogPageSize ||
|
||||
nextRoomRecords.length >= _rewardDialogPageSize;
|
||||
if (_hasMore) {
|
||||
_cursor = requestCursor + 1;
|
||||
}
|
||||
_loading = false;
|
||||
_loadingMore = false;
|
||||
});
|
||||
_rewardDialogDebug(
|
||||
'loaded cursor=$requestCursor '
|
||||
'popupCount=${nextPopupRecords.length} '
|
||||
'popupTotal=${_popupRecords.length} '
|
||||
'roomCount=${nextRoomRecords.length} '
|
||||
'roomTotal=${_roomRecords.length} '
|
||||
'visibleRoomTotal=${_visibleRoomRewardRecords().length} '
|
||||
'hasMore=$_hasMore nextCursor=$_cursor',
|
||||
);
|
||||
} catch (error) {
|
||||
_rewardDialogDebug('request failed: $error');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_loadingMore = false;
|
||||
_hasMore = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SCRoomRocketRewardRecordRes>> _loadPopupRecords(
|
||||
int cursor,
|
||||
) async {
|
||||
_rewardDialogDebug(
|
||||
'request reward-popups roomId=${widget.roomId} '
|
||||
'cursor=$cursor limit=$_rewardDialogPageSize',
|
||||
);
|
||||
final res = await SCChatRoomRepository().roomRocketRewardPopups(
|
||||
widget.roomId,
|
||||
cursor: cursor,
|
||||
limit: _rewardDialogPageSize,
|
||||
);
|
||||
return res.records;
|
||||
}
|
||||
|
||||
Future<List<SCRoomRocketRewardRecordRes>> _loadRoomRewardRecords(
|
||||
int cursor,
|
||||
) async {
|
||||
if (widget.roomId.trim().isEmpty) {
|
||||
return const <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
try {
|
||||
_rewardDialogDebug(
|
||||
'request reward-room-records roomId=${widget.roomId} '
|
||||
'cursor=$cursor limit=$_rewardDialogPageSize',
|
||||
);
|
||||
final res = await SCChatRoomRepository().roomRocketRewardRecords(
|
||||
widget.roomId,
|
||||
cursor: cursor,
|
||||
limit: _rewardDialogPageSize,
|
||||
);
|
||||
return res.records;
|
||||
} catch (error) {
|
||||
_rewardDialogDebug('reward-room-records request failed: $error');
|
||||
return const <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
}
|
||||
|
||||
List<SCRoomRocketRewardRecordRes> _visibleRoomRewardRecords() {
|
||||
if (_roomRecords.isEmpty) {
|
||||
return _popupRecords;
|
||||
}
|
||||
final launchNos =
|
||||
_popupRecords
|
||||
.map((item) => item.launchNo.trim())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet();
|
||||
if (launchNos.isEmpty) {
|
||||
return _roomRecords;
|
||||
}
|
||||
final matched = _roomRecords
|
||||
.where((item) => launchNos.contains(item.launchNo.trim()))
|
||||
.toList(growable: false);
|
||||
return matched.isEmpty ? _roomRecords : matched;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoomRocketRewardDialog(
|
||||
selectedReward: widget.selectedReward,
|
||||
rewards: _rewards,
|
||||
winners: _winners,
|
||||
loadingText: _loading ? 'Loading data......' : '',
|
||||
isLoadingMore: _loadingMore,
|
||||
hasMore: _hasMore,
|
||||
onLoadMore: () => unawaited(_load()),
|
||||
useMockFallback: false,
|
||||
onClose: widget.onClose,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _rewardDialogDebug(String message) {
|
||||
if (!kDebugMode) {
|
||||
return;
|
||||
}
|
||||
debugPrint('[RoomRocketRewardDialog] $message');
|
||||
}
|
||||
@ -20,6 +20,7 @@ import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
||||
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_chat_message.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
|
||||
@ -1167,31 +1168,25 @@ class _MsgItemState extends State<MsgItem> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMedals(List<WearBadge> wearBadge) {
|
||||
return wearBadge.isNotEmpty
|
||||
? Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: 25.w,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
itemCount: wearBadge.length,
|
||||
itemBuilder: (context, index) {
|
||||
return netImage(
|
||||
width: 25.w,
|
||||
height: 25.w,
|
||||
url: wearBadge[index].selectUrl ?? "",
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return SizedBox(width: 5.w);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container();
|
||||
Widget _buildMedals({
|
||||
required String? userId,
|
||||
List<WearBadge> fallbackBadges = const <WearBadge>[],
|
||||
}) {
|
||||
final hasUserId = (userId ?? "").trim().isNotEmpty;
|
||||
if (!hasUserId && fallbackBadges.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
return Expanded(
|
||||
child: SCUserBadgeStrip(
|
||||
userId: userId,
|
||||
fallbackBadges: fallbackBadges,
|
||||
height: 25.w,
|
||||
badgeHeight: 25.w,
|
||||
longBadgeWidth: 55.w,
|
||||
spacing: 5.w,
|
||||
maxBadges: 8,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _msgColor(String type) {
|
||||
@ -1773,10 +1768,12 @@ class _MsgItemState extends State<MsgItem> {
|
||||
)
|
||||
: Container(),
|
||||
_buildMedals(
|
||||
(widget.msg.user?.wearBadge?.where((item) {
|
||||
return item.use ?? false;
|
||||
}).toList() ??
|
||||
[]),
|
||||
userId: widget.msg.user?.id,
|
||||
fallbackBadges:
|
||||
widget.msg.user?.wearBadge
|
||||
?.where((item) => item.use ?? false)
|
||||
.toList() ??
|
||||
const <WearBadge>[],
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -1853,10 +1850,12 @@ class _MsgItemState extends State<MsgItem> {
|
||||
)
|
||||
: Container(),
|
||||
_buildMedals(
|
||||
user.wearBadge?.where((item) {
|
||||
return item.use ?? false;
|
||||
}).toList() ??
|
||||
[],
|
||||
userId: user.id,
|
||||
fallbackBadges:
|
||||
user.wearBadge
|
||||
?.where((item) => item.use ?? false)
|
||||
.toList() ??
|
||||
const <WearBadge>[],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
267
lib/ui_kit/widgets/room/room_password_dialog.dart
Normal file
@ -0,0 +1,267 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
|
||||
typedef RoomPasswordSubmit = Future<String?> Function(String password);
|
||||
|
||||
class RoomPasswordDialog extends StatefulWidget {
|
||||
static const String dialogTag = 'showRoomPasswordDialog';
|
||||
|
||||
const RoomPasswordDialog({
|
||||
super.key,
|
||||
required this.onSubmit,
|
||||
required this.onCancel,
|
||||
required this.onSuccess,
|
||||
});
|
||||
|
||||
final RoomPasswordSubmit onSubmit;
|
||||
final VoidCallback onCancel;
|
||||
final ValueChanged<String> onSuccess;
|
||||
|
||||
static Future<String?> show({required RoomPasswordSubmit onSubmit}) {
|
||||
final completer = Completer<String?>();
|
||||
var isCompleted = false;
|
||||
|
||||
void complete(String? password) {
|
||||
if (isCompleted) {
|
||||
return;
|
||||
}
|
||||
isCompleted = true;
|
||||
completer.complete(password);
|
||||
}
|
||||
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
SmartDialog.show(
|
||||
tag: dialogTag,
|
||||
alignment: Alignment.center,
|
||||
animationType: SmartAnimationType.fade,
|
||||
backType: SmartBackType.block,
|
||||
clickMaskDismiss: false,
|
||||
maskColor: Colors.black.withValues(alpha: 0.58),
|
||||
onDismiss: () {
|
||||
complete(null);
|
||||
},
|
||||
builder: (_) {
|
||||
return RoomPasswordDialog(
|
||||
onSubmit: onSubmit,
|
||||
onCancel: () {
|
||||
complete(null);
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
},
|
||||
onSuccess: (password) {
|
||||
complete(password);
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
State<RoomPasswordDialog> createState() => _RoomPasswordDialogState();
|
||||
}
|
||||
|
||||
class _RoomPasswordDialogState extends State<RoomPasswordDialog> {
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
String _errorText = "";
|
||||
bool _isSubmitting = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleEnter() async {
|
||||
if (_isSubmitting) {
|
||||
return;
|
||||
}
|
||||
final password = _passwordController.text.trim();
|
||||
if (password.isEmpty) {
|
||||
setState(() {
|
||||
_errorText = "Please enter password";
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_errorText = "";
|
||||
_isSubmitting = true;
|
||||
});
|
||||
final errorText = await widget.onSubmit(password);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if ((errorText ?? "").trim().isNotEmpty) {
|
||||
setState(() {
|
||||
_errorText = errorText!.trim();
|
||||
_isSubmitting = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
widget.onSuccess(password);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 316.w,
|
||||
padding: EdgeInsets.fromLTRB(18.w, 18.w, 18.w, 16.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff09372E),
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Room lock",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18.w),
|
||||
_buildPasswordInput(),
|
||||
SizedBox(height: _errorText.isEmpty ? 14.w : 8.w),
|
||||
if (_errorText.isNotEmpty) _buildErrorText(),
|
||||
SizedBox(height: 12.w),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildButton(
|
||||
label: "cancel",
|
||||
filled: false,
|
||||
onTap: _isSubmitting ? null : widget.onCancel,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: _buildButton(
|
||||
label: "enter",
|
||||
filled: true,
|
||||
onTap: _isSubmitting ? null : _handleEnter,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordInput() {
|
||||
return Container(
|
||||
height: 44.w,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff08251E),
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
border: Border.all(color: const Color(0xffB2FBCC), width: 1),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _passwordController,
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
cursorColor: SocialChatTheme.primaryColor,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
textInputAction: TextInputAction.done,
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(24)],
|
||||
onSubmitted: (_) {
|
||||
_handleEnter();
|
||||
},
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Please enter password",
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
fontSize: 14.sp,
|
||||
height: 16 / 14,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
backgroundColor: Colors.transparent,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 16 / 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorText() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
_errorText,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: const Color(0xffff4d4d),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 14 / 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton({
|
||||
required String label,
|
||||
required bool filled,
|
||||
required VoidCallback? onTap,
|
||||
}) {
|
||||
final child = Container(
|
||||
height: 38.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: filled ? SocialChatTheme.primaryColor : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(19.w),
|
||||
border: Border.all(
|
||||
color: filled ? SocialChatTheme.primaryColor : Colors.white,
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: filled ? Colors.black : Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Opacity(
|
||||
opacity: onTap == null ? 0.55 : 1,
|
||||
child: SCDebounceWidget(onTap: onTap ?? () {}, child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/music/room_music_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/music/room_music_floating_entry.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
|
||||
@ -13,6 +14,7 @@ import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.
|
||||
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_pending_cache.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_banner_view.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_game_bottom_sheet.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart';
|
||||
|
||||
class RoomPlayWidget extends StatefulWidget {
|
||||
const RoomPlayWidget({super.key});
|
||||
@ -22,6 +24,8 @@ class RoomPlayWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RoomPlayWidgetState extends State<RoomPlayWidget> {
|
||||
static const double _rightActionDownShift = 24;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.expand(
|
||||
@ -29,17 +33,28 @@ class _RoomPlayWidgetState extends State<RoomPlayWidget> {
|
||||
children: [
|
||||
PositionedDirectional(
|
||||
end: 15.w,
|
||||
bottom: 150.w,
|
||||
bottom: (150 - _rightActionDownShift).w,
|
||||
child: const RoomMusicFloatingEntry(),
|
||||
),
|
||||
Selector<RoomMusicManager, bool>(
|
||||
selector: (_, manager) => manager.current != null,
|
||||
builder: (context, hasCurrentMusic, child) {
|
||||
return PositionedDirectional(
|
||||
end: (hasCurrentMusic ? 65 : 15).w,
|
||||
bottom: (150 - _rightActionDownShift).w,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: const RoomRocketFloatingEntry(),
|
||||
),
|
||||
PositionedDirectional(
|
||||
end: 15.w,
|
||||
bottom: 200.w,
|
||||
bottom: (200 - _rightActionDownShift).w,
|
||||
child: const _RoomRedPacketFloatingEntry(),
|
||||
),
|
||||
PositionedDirectional(
|
||||
end: 15.w,
|
||||
bottom: 100.w,
|
||||
bottom: (100 - _rightActionDownShift).w,
|
||||
child: const RoomGameEntryButton(),
|
||||
),
|
||||
PositionedDirectional(
|
||||
|
||||
@ -52,8 +52,6 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final SVGAAnimationController _controller;
|
||||
String? _loadedResource;
|
||||
MovieEntity? _retainedMovieEntity;
|
||||
String? _retainedResource;
|
||||
bool _hasError = false;
|
||||
|
||||
@override
|
||||
@ -80,10 +78,9 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
final resource = widget.resource.trim();
|
||||
if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) {
|
||||
setState(() {
|
||||
_controller.videoItem = null;
|
||||
_releaseRetainedMovieEntity();
|
||||
_hasError = true;
|
||||
_loadedResource = null;
|
||||
_controller.videoItem = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -92,53 +89,37 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
_hasError = false;
|
||||
});
|
||||
|
||||
MovieEntity? acquiredMovieEntity;
|
||||
try {
|
||||
final movieEntity =
|
||||
_retainedResource == resource && _retainedMovieEntity != null
|
||||
? _retainedMovieEntity!
|
||||
: await SCGiftVapSvgaManager().acquireSvgaEntity(resource);
|
||||
final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity);
|
||||
if (acquiredNewEntity) {
|
||||
acquiredMovieEntity = movieEntity;
|
||||
}
|
||||
if (!mounted || widget.resource.trim() != resource) {
|
||||
if (acquiredNewEntity) {
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
|
||||
acquiredMovieEntity = null;
|
||||
}
|
||||
return;
|
||||
final cache = SCGiftVapSvgaManager().videoItemCache;
|
||||
var movieEntity = cache[resource];
|
||||
if (movieEntity == null) {
|
||||
movieEntity =
|
||||
resource.startsWith("http")
|
||||
? await SVGAParser.shared.decodeFromURL(resource)
|
||||
: await SVGAParser.shared.decodeFromAssets(resource);
|
||||
movieEntity.autorelease = false;
|
||||
cache[resource] = movieEntity;
|
||||
}
|
||||
if (widget.movieConfigurer != null) {
|
||||
movieEntity.dynamicItem.reset();
|
||||
await widget.movieConfigurer!(movieEntity);
|
||||
}
|
||||
if (!mounted || widget.resource.trim() != resource) {
|
||||
if (acquiredNewEntity) {
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity);
|
||||
acquiredMovieEntity = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_releaseRetainedMovieEntity(except: movieEntity);
|
||||
_retainedMovieEntity = movieEntity;
|
||||
_retainedResource = resource;
|
||||
_loadedResource = resource;
|
||||
_controller.videoItem = movieEntity;
|
||||
});
|
||||
acquiredMovieEntity = null;
|
||||
_syncPlayback(restartIfActive: true);
|
||||
} catch (error) {
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity);
|
||||
if (!mounted || widget.resource.trim() != resource) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_controller.videoItem = null;
|
||||
_releaseRetainedMovieEntity();
|
||||
_hasError = true;
|
||||
_loadedResource = null;
|
||||
_controller.videoItem = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -164,20 +145,9 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_releaseRetainedMovieEntity();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _releaseRetainedMovieEntity({MovieEntity? except}) {
|
||||
final entity = _retainedMovieEntity;
|
||||
if (entity == null || identical(entity, except)) {
|
||||
return;
|
||||
}
|
||||
_retainedMovieEntity = null;
|
||||
_retainedResource = null;
|
||||
SCGiftVapSvgaManager().releaseSvgaEntity(entity);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final videoItem = _controller.videoItem;
|
||||
|
||||
@ -8,11 +8,9 @@ typedef SCSvgaMovieConfigurer =
|
||||
FutureOr<void> Function(MovieEntity movieEntity);
|
||||
|
||||
class SCSvgaAssetWidget extends StatefulWidget {
|
||||
static const int _maxCacheEntries = 8;
|
||||
static final Map<String, MovieEntity> _cache = <String, MovieEntity>{};
|
||||
static final Map<String, Future<MovieEntity>> _loadingTasks =
|
||||
<String, Future<MovieEntity>>{};
|
||||
static final Map<MovieEntity, int> _activeUseCounts = <MovieEntity, int>{};
|
||||
|
||||
const SCSvgaAssetWidget({
|
||||
super.key,
|
||||
@ -51,26 +49,8 @@ class SCSvgaAssetWidget extends StatefulWidget {
|
||||
return _obtainMovieEntity(assetPath);
|
||||
}
|
||||
|
||||
static void clearMemoryCache({bool includeActive = false}) {
|
||||
final entries = _cache.entries.toList(growable: false);
|
||||
for (final entry in entries) {
|
||||
final entity = entry.value;
|
||||
if (!includeActive && _isActive(entity)) {
|
||||
continue;
|
||||
}
|
||||
_cache.remove(entry.key);
|
||||
_disposeMovieEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<MovieEntity> _acquireMovieEntity(String assetPath) async {
|
||||
final entity = await _obtainMovieEntity(assetPath);
|
||||
_retainMovieEntity(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
static Future<MovieEntity> _obtainMovieEntity(String assetPath) async {
|
||||
final cached = _touchCachedMovieEntity(assetPath);
|
||||
final cached = _cache[assetPath];
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
@ -91,7 +71,7 @@ class SCSvgaAssetWidget extends StatefulWidget {
|
||||
);
|
||||
}
|
||||
entity.autorelease = false;
|
||||
_cacheMovieEntity(assetPath, entity);
|
||||
_cache[assetPath] = entity;
|
||||
return entity;
|
||||
}();
|
||||
|
||||
@ -103,77 +83,6 @@ class SCSvgaAssetWidget extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
static MovieEntity? _touchCachedMovieEntity(String assetPath) {
|
||||
final cached = _cache.remove(assetPath);
|
||||
if (cached != null) {
|
||||
_cache[assetPath] = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
static void _cacheMovieEntity(String assetPath, MovieEntity entity) {
|
||||
final previous = _cache.remove(assetPath);
|
||||
if (previous != null && !identical(previous, entity)) {
|
||||
_disposeMovieEntityIfInactive(previous);
|
||||
}
|
||||
_cache[assetPath] = entity;
|
||||
_trimCache();
|
||||
}
|
||||
|
||||
static void _trimCache() {
|
||||
while (_cache.length > _maxCacheEntries) {
|
||||
final removableEntry = _cache.entries.firstWhere(
|
||||
(entry) => !_isActive(entry.value),
|
||||
orElse: () => _cache.entries.first,
|
||||
);
|
||||
if (_isActive(removableEntry.value)) {
|
||||
_cache.remove(removableEntry.key);
|
||||
_cache[removableEntry.key] = removableEntry.value;
|
||||
break;
|
||||
}
|
||||
_cache.remove(removableEntry.key);
|
||||
_disposeMovieEntity(removableEntry.value);
|
||||
}
|
||||
}
|
||||
|
||||
static void _retainMovieEntity(MovieEntity entity) {
|
||||
_activeUseCounts[entity] = (_activeUseCounts[entity] ?? 0) + 1;
|
||||
}
|
||||
|
||||
static void _releaseMovieEntity(MovieEntity? entity) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
final count = _activeUseCounts[entity] ?? 0;
|
||||
if (count <= 1) {
|
||||
_activeUseCounts.remove(entity);
|
||||
if (!_cache.containsValue(entity)) {
|
||||
_disposeMovieEntity(entity);
|
||||
} else {
|
||||
_trimCache();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_activeUseCounts[entity] = count - 1;
|
||||
}
|
||||
|
||||
static bool _isActive(MovieEntity entity) =>
|
||||
(_activeUseCounts[entity] ?? 0) > 0;
|
||||
|
||||
static void _disposeMovieEntityIfInactive(MovieEntity entity) {
|
||||
if (_isActive(entity)) {
|
||||
return;
|
||||
}
|
||||
_disposeMovieEntity(entity);
|
||||
}
|
||||
|
||||
static void _disposeMovieEntity(MovieEntity entity) {
|
||||
try {
|
||||
_activeUseCounts.remove(entity);
|
||||
entity.dispose();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
State<SCSvgaAssetWidget> createState() => _SCSvgaAssetWidgetState();
|
||||
}
|
||||
@ -182,8 +91,6 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final SVGAAnimationController _controller;
|
||||
String? _loadedAssetPath;
|
||||
MovieEntity? _retainedMovieEntity;
|
||||
String? _retainedAssetPath;
|
||||
bool _hasError = false;
|
||||
|
||||
@override
|
||||
@ -224,44 +131,25 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
|
||||
setState(() {
|
||||
_hasError = false;
|
||||
});
|
||||
MovieEntity? acquiredMovieEntity;
|
||||
try {
|
||||
final movieEntity =
|
||||
_retainedAssetPath == assetPath && _retainedMovieEntity != null
|
||||
? _retainedMovieEntity!
|
||||
: await SCSvgaAssetWidget._acquireMovieEntity(assetPath);
|
||||
final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity);
|
||||
if (acquiredNewEntity) {
|
||||
acquiredMovieEntity = movieEntity;
|
||||
}
|
||||
final movieEntity = await SCSvgaAssetWidget._obtainMovieEntity(assetPath);
|
||||
if (widget.movieConfigurer != null) {
|
||||
movieEntity.dynamicItem.reset();
|
||||
await widget.movieConfigurer!(movieEntity);
|
||||
}
|
||||
if (!mounted || widget.assetPath != assetPath) {
|
||||
if (acquiredNewEntity) {
|
||||
SCSvgaAssetWidget._releaseMovieEntity(movieEntity);
|
||||
acquiredMovieEntity = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_releaseRetainedMovieEntity(except: movieEntity);
|
||||
_retainedMovieEntity = movieEntity;
|
||||
_retainedAssetPath = assetPath;
|
||||
_loadedAssetPath = assetPath;
|
||||
_controller.videoItem = movieEntity;
|
||||
});
|
||||
acquiredMovieEntity = null;
|
||||
_syncPlayback(restartIfActive: true);
|
||||
} catch (error) {
|
||||
SCSvgaAssetWidget._releaseMovieEntity(acquiredMovieEntity);
|
||||
if (!mounted || widget.assetPath != assetPath) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_controller.videoItem = null;
|
||||
_releaseRetainedMovieEntity();
|
||||
_hasError = true;
|
||||
});
|
||||
}
|
||||
@ -297,20 +185,9 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
|
||||
void dispose() {
|
||||
_controller.removeStatusListener(_handleAnimationStatusChanged);
|
||||
_controller.dispose();
|
||||
_releaseRetainedMovieEntity();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _releaseRetainedMovieEntity({MovieEntity? except}) {
|
||||
final entity = _retainedMovieEntity;
|
||||
if (entity == null || identical(entity, except)) {
|
||||
return;
|
||||
}
|
||||
_retainedMovieEntity = null;
|
||||
_retainedAssetPath = null;
|
||||
SCSvgaAssetWidget._releaseMovieEntity(entity);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_hasError) {
|
||||
|
||||
40
pubspec.lock
@ -635,6 +635,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
flutter_svga:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1137,6 +1145,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
pag:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pag
|
||||
sha256: "8ba3b9c18309b0ef71ad53d20c67ed13893ad3cc49fdb7cc48a8d4e78bfe6db7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.7"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1645,6 +1661,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "4d35a36400983c3457c289d4d553b5308f506ea84f7e51c7a564651b5525209a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "98e7e94de127b46a86ef46197fff84ff99f3d3b80a708390d717ad731efef598"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.3.1+12
|
||||
version: 1.4.0+140
|
||||
|
||||
|
||||
environment:
|
||||
@ -133,6 +133,8 @@ dependencies:
|
||||
readmore: ^3.0.0
|
||||
#外链跳转
|
||||
app_links: ^6.4.1
|
||||
flutter_svg: ^2.3.0
|
||||
pag: ^1.0.7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
@ -166,10 +168,13 @@ flutter:
|
||||
- sc_images/daily_sign_in/
|
||||
- sc_images/register_reward/
|
||||
- sc_images/room/
|
||||
- sc_images/room/cp_progress/
|
||||
- sc_images/room/cp_invite/
|
||||
- sc_images/room/background_examples/
|
||||
- sc_images/room/emoji/
|
||||
- sc_images/room/entrance/
|
||||
- sc_images/room/red_packet/
|
||||
- sc_images/room/rocket/
|
||||
- sc_images/room/anim/
|
||||
- sc_images/room/anim/gift/
|
||||
- sc_images/room/anim/luck_gift/
|
||||
|
||||
BIN
sc_images/room/cp_invite/sc_cp_invite_accept_btn.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_avatar_frame.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_background.png
Normal file
|
After Width: | Height: | Size: 706 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_close.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_countdown.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_divider.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_gift.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_gift_burst.png
Normal file
|
After Width: | Height: | Size: 970 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_gift_floor.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_panel.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_reject_btn.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
sc_images/room/cp_invite/sc_cp_invite_title.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
sc_images/room/cp_progress/sc_cp_progress_avatar_frame.png
Normal file
|
After Width: | Height: | Size: 570 KiB |
BIN
sc_images/room/cp_progress/sc_cp_progress_bar_bg.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
sc_images/room/cp_progress/sc_cp_progress_bar_fill.png
Normal file
|
After Width: | Height: | Size: 24 KiB |