Compare commits

...

3 Commits

Author SHA1 Message Date
roxy
77d1fd13b1 1.4 2026-05-15 17:43:49 +08:00
roxy
bd6d35ee1b 🚀 UI 2026-05-14 17:10:53 +08:00
roxy
3103d51d4c 1.3.1 2026-05-13 13:46:50 +08:00
126 changed files with 17068 additions and 4052 deletions

View File

@ -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": "إطلاق الصاروخ"
}

View File

@ -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": "রকেট চালু করুন"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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

View File

@ -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 = "";

View File

@ -46,6 +46,9 @@ abstract class AppConfig {
/// URL
String get adminUrl;
/// Yumi管理中心URL
String get managerCenterUrl;
/// URL
String get agencyCenterUrl;

View File

@ -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,

View File

@ -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;

View File

@ -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";

View File

@ -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');

View 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',
),
),
],
),
);
}
}

View 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),
),
);
}
}

View 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)),
),
);
}
}

View 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: () {},
),
),
],
),
);
}
}

View 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)],
),
),
);
}
}

View 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(),
);
}
}

View 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)],
),
),
);
}
}

View 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)],
),
),
);
}
}

View File

@ -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) {

View File

@ -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([

View File

@ -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,
);
}
}

View File

@ -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>[];

View File

@ -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) {

View File

@ -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) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@ class _RoomOnlinePageState
if (dialogContext == null) {
return;
}
showBottomInCenterDialog(dialogContext, RoomUserInfoCard(userId: userId));
showBottomInBottomDialog(dialogContext, RoomUserInfoCard(userId: userId));
});
}

View File

@ -98,7 +98,7 @@ class _RoomGiftRankTabPageState
// headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl,
),
onTap: () {
showBottomInCenterDialog(
showBottomInBottomDialog(
navigatorKey.currentState!.context,
RoomUserInfoCard(userId: userProfile?.id),
);

File diff suppressed because it is too large Load Diff

View 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(

View File

@ -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(

View File

@ -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,

View File

@ -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);

View File

@ -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);
}

View File

@ -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),

View File

@ -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();
}

View File

@ -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)}',
);
}
// 500Vue3初始化
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();
}
}

View File

@ -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),
);

View File

@ -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"];

View File

@ -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);

View File

@ -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

View File

@ -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获取礼物

View File

@ -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;
}

View File

@ -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}) {

View 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;
}

View File

@ -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);
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 (_) {}
}

View File

@ -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',

View File

@ -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, {

View File

@ -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,
),
],
),
),
),
),
);
}
}

View 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;
}
}

View File

@ -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) {

View File

@ -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) {
//

View 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,
),
),
),
],
),
),
);
}
}

View 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,
),
),
],
),
);
}
}

View File

@ -247,7 +247,7 @@ class EmptyMaiSelect extends StatelessWidget {
context,
() {
Navigator.of(context).pop();
showBottomInCenterDialog(
showBottomInBottomDialog(
context,
RoomUserInfoCard(userId: clickUser?.id),
);

View File

@ -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',
);

View File

@ -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();
// 23
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';
}

View 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 '';
}
}

File diff suppressed because it is too large Load Diff

View 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)}...';
}

View 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;

View File

@ -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://');
}
}

View 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;

File diff suppressed because it is too large Load Diff

View 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');
}

View File

@ -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>[],
),
],
),

View 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),
);
}
}

View File

@ -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(

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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) {

View File

@ -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:

View File

@ -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/

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Some files were not shown because too many files have changed in this diff Show More