diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
index 5b6c7c4..e6b1e21 100644
--- a/android/app/proguard-rules.pro
+++ b/android/app/proguard-rules.pro
@@ -27,11 +27,15 @@
-keep class com.google.android.play.core.tasks.** { *; }
-keep class io.flutter.embedding.engine.deferredcomponents.** { *; }
-# 应用特定
+# 应用特定
-keep class com.org.yumiparty.** { *; }
-
-# Provider
--keep class * extends ChangeNotifier { *; }
+
+# Tencent TRTC
+-keep class com.tencent.** { *; }
+-dontwarn com.tencent.**
+
+# Provider
+-keep class * extends ChangeNotifier { *; }
-keep class * implements Listenable { *; }
# 其他保留
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index d2634e7..287663c 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,6 +15,7 @@
+
-
+
13.2.20652)
- TOCropViewController (2.6.1)
+ - TXCustomBeautyProcesserPlugin (1.0.2)
- TXIMSDK_Plus_iOS_XCFramework (8.3.6498)
+ - TXLiteAVSDK_Professional (13.2.20652):
+ - TXLiteAVSDK_Professional/Professional (= 13.2.20652)
+ - TXLiteAVSDK_Professional/Professional (13.2.20652)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
@@ -262,7 +222,6 @@ PODS:
- FlutterMacOS
DEPENDENCIES:
- - agora_rtc_engine (from `.symlinks/plugins/agora_rtc_engine/ios`)
- app_links (from `.symlinks/plugins/app_links/ios`)
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@@ -276,7 +235,6 @@ DEPENDENCIES:
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- - 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`)
@@ -286,6 +244,7 @@ DEPENDENCIES:
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- tancent_vap (from `.symlinks/plugins/tancent_vap/ios`)
- tencent_cloud_chat_sdk (from `.symlinks/plugins/tencent_cloud_chat_sdk/ios`)
+ - tencent_rtc_sdk (from `.symlinks/plugins/tencent_rtc_sdk/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
@@ -294,9 +253,6 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- - AgoraInfra_iOS
- - AgoraIrisRTC_iOS
- - AgoraRtcEngine_iOS
- AppAuth
- AppCheckCore
- Firebase
@@ -327,11 +283,11 @@ SPEC REPOS:
- SDWebImageWebPCoder
- SwiftyBeaver
- TOCropViewController
+ - TXCustomBeautyProcesserPlugin
- TXIMSDK_Plus_iOS_XCFramework
+ - TXLiteAVSDK_Professional
EXTERNAL SOURCES:
- agora_rtc_engine:
- :path: ".symlinks/plugins/agora_rtc_engine/ios"
app_links:
:path: ".symlinks/plugins/app_links/ios"
audioplayers_darwin:
@@ -358,8 +314,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
- iris_method_channel:
- :path: ".symlinks/plugins/iris_method_channel/ios"
on_audio_query_ios:
:path: ".symlinks/plugins/on_audio_query_ios/ios"
package_info_plus:
@@ -378,6 +332,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/tancent_vap/ios"
tencent_cloud_chat_sdk:
:path: ".symlinks/plugins/tencent_cloud_chat_sdk/ios"
+ tencent_rtc_sdk:
+ :path: ".symlinks/plugins/tencent_rtc_sdk/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
@@ -390,10 +346,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
- agora_rtc_engine: 0c7d50312967c4dc31c3c45e50589ce48f57e08a
- AgoraInfra_iOS: 3691b2b277a1712a35ae96de25af319de0d73d08
- AgoraIrisRTC_iOS: eab58c126439adf5ec99632828a558ea216860da
- AgoraRtcEngine_iOS: 97e2398a2addda9057815a2a583a658e36796ff6
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
@@ -426,7 +378,6 @@ SPEC CHECKSUMS:
image_cropper: 655b3ba703c9e15e3111e79151624d6154288774
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
- iris_method_channel: 7d661cf3259b3009ae423508470dbeb9374446ee
libpag: 98742fad4b3ac2a2ee31e383d2483495fb9a5fb4
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
@@ -446,8 +397,11 @@ SPEC CHECKSUMS:
SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82
tancent_vap: fa8ad93814a9f950514a7074c662d2c937084c68
tencent_cloud_chat_sdk: 55e5fffe20f6b7937a26a674ccccb639563a9790
+ tencent_rtc_sdk: f77558b6b436a149d378557c2a837f73c09061bc
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
+ TXCustomBeautyProcesserPlugin: 099393b941cb40eda12b3a80bf6c0319957b1cfd
TXIMSDK_Plus_iOS_XCFramework: 5d1933192fb3b7ef2fe933f1623de4a0486a7fe2
+ TXLiteAVSDK_Professional: e792e6084a208ecef6196c5f28237ad8078b4d7c
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
diff --git a/lib/app/config/app_config.dart b/lib/app/config/app_config.dart
index d4688f5..5e224ea 100644
--- a/lib/app/config/app_config.dart
+++ b/lib/app/config/app_config.dart
@@ -73,8 +73,8 @@ abstract class AppConfig {
/// 腾讯IM App ID
String get tencentImAppid;
- /// Agora RTC App ID
- String get agoraRtcAppid;
+ /// TRTC SDK App ID
+ int get trtcSdkAppId;
/// 游戏App ID
num get gameAppid;
diff --git a/lib/app/config/configs/sc_variant1_config.dart b/lib/app/config/configs/sc_variant1_config.dart
index 38e408c..be7db96 100644
--- a/lib/app/config/configs/sc_variant1_config.dart
+++ b/lib/app/config/configs/sc_variant1_config.dart
@@ -89,7 +89,8 @@ class SCVariant1Config implements AppConfig {
String get tencentImAppid => '20036101';
@override
- String get agoraRtcAppid => '4b5e5cea3b86476caf7f7a57d05b82d1';
+ int get trtcSdkAppId =>
+ const int.fromEnvironment('TRTC_SDK_APP_ID', defaultValue: 20036101);
@override
num get gameAppid => 9999999999; // 需要注册新的游戏服务账户并获取独立App ID
@@ -143,7 +144,7 @@ class SCVariant1Config implements AppConfig {
'managerCenterUrl': managerCenterUrl,
'primaryColor': primaryColor,
'tencentImAppid': tencentImAppid,
- 'agoraRtcAppid': agoraRtcAppid,
+ 'trtcSdkAppId': trtcSdkAppId,
'gameAppid': gameAppid,
'gameAppChannel': gameAppChannel,
'bigBroadcastGroup': bigBroadcastGroup,
@@ -166,6 +167,9 @@ class SCVariant1Config implements AppConfig {
if (apiHost.isEmpty || !apiHost.startsWith('http')) {
errors.add('API主机地址无效或未设置');
}
+ if (trtcSdkAppId <= 0) {
+ errors.add('TRTC SDK App ID 无效或未设置');
+ }
// 如果有错误,根据模式处理
if (errors.isNotEmpty) {
diff --git a/lib/app/constants/sc_global_config.dart b/lib/app/constants/sc_global_config.dart
index 2d9f73f..59cdd01 100644
--- a/lib/app/constants/sc_global_config.dart
+++ b/lib/app/constants/sc_global_config.dart
@@ -55,7 +55,7 @@ class SCGlobalConfig {
static String originChild = "LIKEI";
static String get tencentImAppid => AppConfig.current.tencentImAppid;
- static String get agoraRtcAppid => AppConfig.current.agoraRtcAppid;
+ static int get trtcSdkAppId => AppConfig.current.trtcSdkAppId;
static num get gameAppid => AppConfig.current.gameAppid;
static String get gameAppChannel => AppConfig.current.gameAppChannel;
diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart
index 8f6f70b..01f121b 100644
--- a/lib/app_localizations.dart
+++ b/lib/app_localizations.dart
@@ -8,6 +8,49 @@ class SCAppLocalizations {
SCAppLocalizations(this.locale);
+ static const List supportedLanguageCodes = [
+ 'en',
+ 'ar',
+ 'tr',
+ 'bn',
+ 'it',
+ 'pt',
+ 'fr',
+ ];
+
+ static const List supportedLocales = [
+ Locale('en', ''),
+ Locale('ar', ''),
+ Locale('tr', ''),
+ Locale('bn', ''),
+ Locale('it', ''),
+ Locale('pt', ''),
+ Locale('fr', ''),
+ ];
+
+ static bool isSupportedLanguageCode(String languageCode) {
+ return supportedLanguageCodes.contains(
+ _normalizeLanguageCode(languageCode),
+ );
+ }
+
+ static String resolveSupportedLanguageCode(String languageCode) {
+ final normalizedLanguageCode = _normalizeLanguageCode(languageCode);
+ if (supportedLanguageCodes.contains(normalizedLanguageCode)) {
+ return normalizedLanguageCode;
+ }
+ return 'en';
+ }
+
+ static String _normalizeLanguageCode(String languageCode) {
+ return languageCode
+ .trim()
+ .toLowerCase()
+ .replaceAll('_', '-')
+ .split('-')
+ .first;
+ }
+
static SCAppLocalizations? of(BuildContext context) {
return Localizations.of(context, SCAppLocalizations);
}
@@ -18,8 +61,9 @@ class SCAppLocalizations {
Map? _localizedStrings;
Future load() async {
+ final languageCode = resolveSupportedLanguageCode(locale.languageCode);
final jsonString = await rootBundle.loadString(
- 'assets/l10n/intl_${locale.languageCode}.json',
+ 'assets/l10n/intl_$languageCode.json',
);
final Map jsonMap = json.decode(jsonString);
@@ -1792,7 +1836,7 @@ class _SCAppLocalizationsDelegate
@override
bool isSupported(Locale locale) =>
- ['en', 'zh', 'ar', 'bn', 'tr'].contains(locale.languageCode);
+ SCAppLocalizations.isSupportedLanguageCode(locale.languageCode);
@override
Future load(Locale locale) async {
diff --git a/lib/main.dart b/lib/main.dart
index 6612033..575c942 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -417,13 +417,7 @@ class _YumiApplicationState extends State
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
- supportedLocales: const [
- Locale('en', ''),
- Locale('zh', ''),
- Locale('ar', ''),
- Locale('tr', ''),
- Locale('bn', ''),
- ],
+ supportedLocales: SCAppLocalizations.supportedLocales,
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
onGenerateRoute: SCLkApplication.router.generator,
diff --git a/lib/modules/settings/language/language_page.dart b/lib/modules/settings/language/language_page.dart
index 4523191..66c7ed7 100644
--- a/lib/modules/settings/language/language_page.dart
+++ b/lib/modules/settings/language/language_page.dart
@@ -1,245 +1,156 @@
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_screenutil/flutter_screenutil.dart';
-import 'package:yumi/app_localizations.dart';
-import 'package:provider/provider.dart';
-
-import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
-import 'package:yumi/ui_kit/components/text/sc_text.dart';
-import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
-import 'package:yumi/services/localization/localization_manager.dart';
-import 'package:yumi/app/constants/sc_global_config.dart';
-
-///语言选择
-class LanguagePage extends StatefulWidget {
- @override
- _LanguagePageState createState() => _LanguagePageState();
-}
-
-class _LanguagePageState extends State {
- int selectType = 1;
-
- @override
- void initState() {
- super.initState();
- String langCode = DataPersistence.getLang();
- if (langCode == "ar") {
- selectType = 1;
- }else if(langCode == "tr"){
- selectType = 2;
- }else if(langCode == "bn"){
- selectType = 3;
- } else {
- selectType = 0;
- }
- }
-
- @override
- Widget build(BuildContext context) {
- return Stack(
- children: [
- Image.asset(
- SCGlobalConfig.businessLogicStrategy.getLanguagePageBackgroundImage(),
- width: ScreenUtil().screenWidth,
- height: ScreenUtil().screenHeight,
- fit: BoxFit.fill,
- ),
- Scaffold(
- backgroundColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageScaffoldBackgroundColor(),
- resizeToAvoidBottomInset: false,
- appBar: SocialChatStandardAppBar(
- title: SCAppLocalizations.of(context)!.language,
- actions: [],
- ),
- body: Column(
- children: [
- Row(
- children: [
- SizedBox(width: 15.w),
- Expanded(
- child: text(
- "English",
- textColor: SCGlobalConfig.businessLogicStrategy.getLanguagePagePrimaryTextColor(),
- fontSize: 15.sp,
- ),
- ),
- Checkbox(
- value: selectType == 0,
- checkColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckColor(),
- activeColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxActiveColor(),
- onChanged: (b) {
- if (selectType == 0) {
- selectType = 1;
- } else {
- selectType = 0;
- }
- setState(() {});
- if (b!) {
- Provider.of(
- context,
- listen: false,
- ).changeAppLanguage(const Locale('en', ''));
- }
- },
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderRadius()),
- ),
- side: WidgetStateBorderSide.resolveWith((states) {
- if (states.contains(WidgetState.selected)) {
- return BorderSide(
- color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(),
- width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(),
- ); // 选中时保留蓝色边框
- }
- return BorderSide(
- color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(),
- width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(),
- ); // 未选中时灰色边框
- }),
- ),
- ],
- ),
- Row(
- children: [
- SizedBox(width: 15.w),
- Expanded(
- child: text(
- "العربية",
- textColor: SCGlobalConfig.businessLogicStrategy.getLanguagePagePrimaryTextColor(),
- fontSize: 15.sp,
- ),
- ),
- Checkbox(
- value: selectType == 1,
- checkColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckColor(),
- activeColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxActiveColor(),
- onChanged: (b) {
- if (selectType == 1) {
- selectType = 0;
- } else {
- selectType = 1;
- }
- setState(() {});
- if (b!) {
- Provider.of(
- context,
- listen: false,
- ).changeAppLanguage(const Locale('ar', ''));
- }
- },
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderRadius()),
- ),
- side: WidgetStateBorderSide.resolveWith((states) {
- if (states.contains(WidgetState.selected)) {
- return BorderSide(
- color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(),
- width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(),
- ); // 选中时保留蓝色边框
- }
- return BorderSide(
- color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(),
- width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(),
- ); // 未选中时灰色边框
- }),
- ),
- ],
- ),
- Row(
- children: [
- SizedBox(width: 15.w),
- Expanded(
- child: text(
- "Türkçe",
- textColor: SCGlobalConfig.businessLogicStrategy.getLanguagePagePrimaryTextColor(),
- fontSize: 15.sp,
- ),
- ),
- Checkbox(
- value: selectType == 2,
- checkColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckColor(),
- activeColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxActiveColor(),
- onChanged: (b) {
- if (selectType == 2) {
- selectType = 0;
- } else {
- selectType = 2;
- }
- setState(() {});
- if (b!) {
- Provider.of(
- context,
- listen: false,
- ).changeAppLanguage(const Locale('tr', ''));
- }
- },
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderRadius()),
- ),
- side: WidgetStateBorderSide.resolveWith((states) {
- if (states.contains(WidgetState.selected)) {
- return BorderSide(
- color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(),
- width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(),
- ); // 选中时保留蓝色边框
- }
- return BorderSide(
- color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(),
- width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(),
- ); // 未选中时灰色边框
- }),
- ),
- ],
- ), Row(
- children: [
- SizedBox(width: 15.w),
- Expanded(
- child: text(
- "বাংলা",
- textColor: SCGlobalConfig.businessLogicStrategy.getLanguagePagePrimaryTextColor(),
- fontSize: 15.sp,
- ),
- ),
- Checkbox(
- value: selectType == 3,
- checkColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckColor(),
- activeColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxActiveColor(),
- onChanged: (b) {
- if (selectType == 3) {
- selectType = 0;
- } else {
- selectType = 3;
- }
- setState(() {});
- if (b!) {
- Provider.of(
- context,
- listen: false,
- ).changeAppLanguage(const Locale('bn', ''));
- }
- },
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderRadius()),
- ),
- side: WidgetStateBorderSide.resolveWith((states) {
- if (states.contains(WidgetState.selected)) {
- return BorderSide(
- color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(),
- width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(),
- ); // 选中时保留蓝色边框
- }
- return BorderSide(
- color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(),
- width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(),
- ); // 未选中时灰色边框
- }),
- ),
- ],
- ),
- ],
- ),
- ),
- ],
- );
- }
-}
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:provider/provider.dart';
+import 'package:yumi/app/constants/sc_global_config.dart';
+import 'package:yumi/app_localizations.dart';
+import 'package:yumi/services/localization/localization_manager.dart';
+import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
+import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
+import 'package:yumi/ui_kit/components/text/sc_text.dart';
+
+/// 语言选择
+class LanguagePage extends StatefulWidget {
+ const LanguagePage({super.key});
+
+ @override
+ State createState() => _LanguagePageState();
+}
+
+class _LanguageOption {
+ const _LanguageOption({required this.code, required this.label});
+
+ final String code;
+ final String label;
+}
+
+class _LanguagePageState extends State {
+ static const List<_LanguageOption> _languageOptions = [
+ _LanguageOption(code: 'en', label: 'English'),
+ _LanguageOption(code: 'ar', label: 'العربية'),
+ _LanguageOption(code: 'tr', label: 'Türkçe'),
+ _LanguageOption(code: 'bn', label: 'বাংলা'),
+ _LanguageOption(code: 'it', label: 'Italiano'),
+ _LanguageOption(code: 'pt', label: 'Português'),
+ _LanguageOption(code: 'fr', label: 'Français'),
+ ];
+
+ String _selectedLanguageCode = 'en';
+ bool _didResolveInitialLanguage = false;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ if (!_didResolveInitialLanguage) {
+ _selectedLanguageCode = _resolveCurrentLanguageCode();
+ _didResolveInitialLanguage = true;
+ }
+ }
+
+ String _resolveCurrentLanguageCode() {
+ final persistedLanguageCode = DataPersistence.getLang();
+ if (SCAppLocalizations.isSupportedLanguageCode(persistedLanguageCode)) {
+ return SCAppLocalizations.resolveSupportedLanguageCode(
+ persistedLanguageCode,
+ );
+ }
+ final currentLanguageCode =
+ context.read().locale?.languageCode ?? 'en';
+ return SCAppLocalizations.resolveSupportedLanguageCode(currentLanguageCode);
+ }
+
+ void _selectLanguage(String languageCode) {
+ final resolvedLanguageCode =
+ SCAppLocalizations.resolveSupportedLanguageCode(languageCode);
+ if (_selectedLanguageCode == resolvedLanguageCode) {
+ return;
+ }
+ setState(() {
+ _selectedLanguageCode = resolvedLanguageCode;
+ });
+ context.read().changeAppLanguage(
+ Locale(resolvedLanguageCode, ''),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ Image.asset(
+ SCGlobalConfig.businessLogicStrategy.getLanguagePageBackgroundImage(),
+ width: ScreenUtil().screenWidth,
+ height: ScreenUtil().screenHeight,
+ fit: BoxFit.fill,
+ ),
+ Scaffold(
+ backgroundColor:
+ SCGlobalConfig.businessLogicStrategy
+ .getLanguagePageScaffoldBackgroundColor(),
+ resizeToAvoidBottomInset: false,
+ appBar: SocialChatStandardAppBar(
+ title: SCAppLocalizations.of(context)!.language,
+ actions: [],
+ ),
+ body: ListView.builder(
+ padding: EdgeInsets.only(top: 4.w),
+ itemCount: _languageOptions.length,
+ itemBuilder: (context, index) {
+ return _buildLanguageRow(_languageOptions[index]);
+ },
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildLanguageRow(_LanguageOption option) {
+ final selected = _selectedLanguageCode == option.code;
+ return InkWell(
+ onTap: () => _selectLanguage(option.code),
+ child: Row(
+ children: [
+ SizedBox(width: 15.w),
+ Expanded(
+ child: text(
+ option.label,
+ textColor:
+ SCGlobalConfig.businessLogicStrategy
+ .getLanguagePagePrimaryTextColor(),
+ fontSize: 15.sp,
+ ),
+ ),
+ Checkbox(
+ value: selected,
+ checkColor:
+ SCGlobalConfig.businessLogicStrategy
+ .getLanguagePageCheckColor(),
+ activeColor:
+ SCGlobalConfig.businessLogicStrategy
+ .getLanguagePageCheckboxActiveColor(),
+ onChanged: (checked) {
+ if (checked == true) {
+ _selectLanguage(option.code);
+ }
+ },
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(
+ SCGlobalConfig.businessLogicStrategy
+ .getLanguagePageCheckboxBorderRadius(),
+ ),
+ ),
+ side: WidgetStateBorderSide.resolveWith((states) {
+ return BorderSide(
+ color:
+ SCGlobalConfig.businessLogicStrategy
+ .getLanguagePageCheckboxBorderColor(),
+ width:
+ SCGlobalConfig.businessLogicStrategy
+ .getLanguagePageCheckboxBorderWidth(),
+ );
+ }),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/services/audio/room_rtc_engine_adapter.dart b/lib/services/audio/room_rtc_engine_adapter.dart
new file mode 100644
index 0000000..c28111f
--- /dev/null
+++ b/lib/services/audio/room_rtc_engine_adapter.dart
@@ -0,0 +1,74 @@
+import 'room_rtc_types.dart';
+
+enum RoomRtcEngineKind { trtc }
+
+class RoomRtcEnterRoomConfig {
+ const RoomRtcEnterRoomConfig({
+ required this.identity,
+ required this.trtcSdkAppId,
+ required this.trtcUserSig,
+ this.throwOnError = false,
+ });
+
+ final RoomRtcSessionIdentity identity;
+ final int trtcSdkAppId;
+ final String trtcUserSig;
+ final bool throwOnError;
+
+ bool get hasTrtcCredential =>
+ trtcSdkAppId > 0 && trtcUserSig.trim().isNotEmpty && identity.isValid;
+}
+
+class RoomRtcAnchorConfig {
+ const RoomRtcAnchorConfig({required this.muted});
+
+ final bool muted;
+}
+
+class RoomRtcAudioMixingConfig {
+ const RoomRtcAudioMixingConfig({
+ required this.filePath,
+ required this.loopback,
+ this.cycle = 1,
+ this.startPos = 0,
+ });
+
+ final String filePath;
+ final bool loopback;
+ final int cycle;
+ final int startPos;
+}
+
+abstract class RoomRtcEngineAdapter {
+ RoomRtcEngineKind get kind;
+
+ Future prewarm();
+
+ Future enterRoomAsAudience(RoomRtcEnterRoomConfig config);
+
+ Future switchToAnchor(RoomRtcAnchorConfig config);
+
+ Future switchToAudience();
+
+ void muteAllRemoteAudio(bool muted);
+
+ Future leaveRoom();
+
+ Future release();
+
+ Future startAudioMixing(RoomRtcAudioMixingConfig config);
+
+ Future pauseAudioMixing();
+
+ Future resumeAudioMixing();
+
+ Future stopAudioMixing();
+
+ Future setAudioMixingPosition(int positionMs);
+
+ Future getAudioMixingPosition();
+
+ Future getAudioMixingDuration();
+
+ Future setAudioMixingVolume(int volume);
+}
diff --git a/lib/services/audio/room_rtc_types.dart b/lib/services/audio/room_rtc_types.dart
new file mode 100644
index 0000000..89eb3f6
--- /dev/null
+++ b/lib/services/audio/room_rtc_types.dart
@@ -0,0 +1,88 @@
+import 'package:yumi/shared/business_logic/models/res/login_res.dart';
+import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'
+ as join_room;
+import 'package:yumi/shared/business_logic/models/res/room_res.dart';
+
+enum RoomRtcAudioMixingState { playing, paused, stopped, failed }
+
+enum RoomRtcAudioMixingReason {
+ ok,
+ allLoopsCompleted,
+ oneLoopCompleted,
+ stoppedByUser,
+ failed,
+ other,
+}
+
+enum RoomRtcExitReason { local, kicked, dismissed, unknown }
+
+class RoomRtcUserVolume {
+ const RoomRtcUserVolume({
+ required this.userId,
+ required this.volume,
+ required this.isLocalUser,
+ });
+
+ final String userId;
+ final int volume;
+ final bool isLocalUser;
+}
+
+typedef RoomMusicMixingStateListener =
+ void Function(
+ RoomRtcAudioMixingState state,
+ RoomRtcAudioMixingReason reason,
+ );
+typedef RoomMusicMicRouteChangedListener = void Function(bool isOnMic);
+typedef RoomRtcVoiceVolumeListener =
+ void Function(List userVolumes, int totalVolume);
+typedef RoomRtcRoomErrorListener = void Function(Object error);
+typedef RoomRtcRoomExitListener = void Function(RoomRtcExitReason reason);
+
+class RoomRtcSessionIdentity {
+ const RoomRtcSessionIdentity({
+ required this.businessRoomId,
+ required this.trtcStrRoomId,
+ required this.trtcUserId,
+ });
+
+ factory RoomRtcSessionIdentity.fromRoom({
+ required SocialChatRoomRes? roomProfile,
+ required SocialChatUserProfile? userProfile,
+ }) {
+ return RoomRtcSessionIdentity.fromValues(
+ businessRoomId: roomProfile?.id,
+ trtcStrRoomId: roomProfile?.roomAccount,
+ trtcUserId: userProfile?.id,
+ );
+ }
+
+ factory RoomRtcSessionIdentity.fromJoinRoom({
+ required join_room.RoomProfile2? roomProfile,
+ required SocialChatUserProfile? userProfile,
+ }) {
+ return RoomRtcSessionIdentity.fromValues(
+ businessRoomId: roomProfile?.id,
+ trtcStrRoomId: roomProfile?.roomAccount,
+ trtcUserId: userProfile?.id,
+ );
+ }
+
+ factory RoomRtcSessionIdentity.fromValues({
+ required String? businessRoomId,
+ required String? trtcStrRoomId,
+ required String? trtcUserId,
+ }) {
+ return RoomRtcSessionIdentity(
+ businessRoomId: (businessRoomId ?? '').trim(),
+ trtcStrRoomId: (trtcStrRoomId ?? '').trim(),
+ trtcUserId: (trtcUserId ?? '').trim(),
+ );
+ }
+
+ final String businessRoomId;
+ final String trtcStrRoomId;
+ final String trtcUserId;
+
+ bool get isValid => trtcStrRoomId.isNotEmpty && trtcUserId.isNotEmpty;
+}
diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart
index 91a4a6c..bff0250 100644
--- a/lib/services/audio/rtc_manager.dart
+++ b/lib/services/audio/rtc_manager.dart
@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:dio/dio.dart';
-import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/app_localizations.dart';
@@ -15,7 +14,10 @@ import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
import 'package:yumi/services/room/rc_room_manager.dart';
+import 'package:yumi/services/audio/room_rtc_engine_adapter.dart';
+import 'package:yumi/services/audio/room_rtc_types.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
+import 'package:yumi/services/audio/trtc_room_rtc_engine_adapter.dart';
import 'package:yumi/services/audio/voice_room_foreground_service.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
@@ -58,14 +60,13 @@ import '../../ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart';
typedef OnSoundVoiceChange = Function(num index, int volum);
typedef RtcProvider = RealTimeCommunicationManager;
-typedef RoomMusicMixingStateListener =
- void Function(AudioMixingStateType state, AudioMixingReasonType reason);
-typedef RoomMusicMicRouteChangedListener = void Function(bool isOnMic);
enum RoomStartupStatus { idle, loading, ready, failed }
enum RoomStartupFailureType { none, entry, im, rtc }
+enum _RoomRtcLocalAudioRole { audience, anchor }
+
class _RoomStartupAsyncResult {
const _RoomStartupAsyncResult.value(this.value)
: error = null,
@@ -90,15 +91,6 @@ class _RoomStartupAsyncResult {
}
}
-class _AgoraJoinCanceled implements Exception {
- const _AgoraJoinCanceled(this.reason);
-
- final String reason;
-
- @override
- String toString() => reason;
-}
-
enum _PendingRoomStartupSeatActionType { clickSeat, goUpMic }
class RoomEntryPreviewData {
@@ -310,8 +302,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
static const Duration _giftTriggeredMicRefreshMinInterval = Duration(
milliseconds: 900,
);
- static const Duration _agoraJoinTimeout = Duration(seconds: 8);
- static const Duration _agoraDisconnectedGracePeriod = Duration(seconds: 12);
static const Duration _roomMusicPublishingStateTtl = Duration(seconds: 15);
static const String _roomStartupSeatLoadingTag =
'roomStartupSeatInteractionLoading';
@@ -327,11 +317,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool _roomVisualEffectsEnabled = false;
bool _voiceRoomRouteVisible = false;
bool _isExitingCurrentVoiceRoomSession = false;
- bool _isHandlingAgoraRoomFailure = false;
- bool _agoraJoined = false;
- bool _agoraRoleIsBroadcaster = false;
- String? _agoraJoinedChannelId;
- bool _agoraAudioConfigured = false;
+ bool _isHandlingRoomRtcFailure = false;
+ bool _roomRtcJoined = false;
+ bool _roomRtcRoleIsBroadcaster = false;
RoomStartupStatus _roomStartupStatus = RoomStartupStatus.idle;
RoomStartupFailureType _roomStartupFailureType = RoomStartupFailureType.none;
int _roomStartupFailureToken = 0;
@@ -341,16 +329,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool _isHandlingRoomStartupFailure = false;
Timer? _micListPollingTimer;
Timer? _onlineUsersPollingTimer;
- Timer? _joinAgoraTimeoutTimer;
- Timer? _agoraDisconnectedCleanupTimer;
Timer? _roomEntryEffectTimer;
Timer? _roomRocketLaunchAnimationTimer;
Timer? _roomRedPacketPresenceTimer;
- Completer? _joinAgoraCompleter;
- Future? _rtcEngineInitTask;
Future? _rtcEnginePrewarmTask;
- Future? _pendingRoomSwitchAgoraLeaveTask;
- String? _joiningChannelId;
+ Future? _pendingRoomSwitchRtcLeaveTask;
bool _isRefreshingMicList = false;
bool _isRefreshingOnlineUsers = false;
bool _disableMicListRefreshForCurrentSession = false;
@@ -369,11 +352,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
int? _pendingSelfMicSwitchGuardUntilMs;
num? _pendingSelfMicReleaseIndex;
int? _pendingSelfMicReleaseGuardUntilMs;
- ClientRoleType? _lastAppliedClientRole;
+ _RoomRtcLocalAudioRole? _lastAppliedLocalAudioRole;
bool? _lastAppliedLocalAudioMuted;
bool? _lastAppliedRecordingSignalMuted;
int? _lastAppliedRecordingSignalVolume;
- String? _lastAppliedRtcToken;
bool? _lastScheduledVoiceLiveOnMic;
String? _lastScheduledVoiceLiveRoomId;
String? _lastScheduledAnchorRoomId;
@@ -391,8 +373,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final Set _roomMusicPublishingUserIds = {};
final Map _roomMusicPublishingClearTimers = {};
- RtcEngine? engine;
- RtcEngineEventHandler? _rtcEngineEventHandler;
+ RoomRtcEngineAdapter? _roomRtcEngineAdapter;
RoomMusicMixingStateListener? _roomMusicMixingStateListener;
RoomMusicMicRouteChangedListener? _roomMusicMicRouteChangedListener;
BuildContext? context;
@@ -1055,54 +1036,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return false;
}
- bool _isCurrentAgoraChannel(String? channelId) {
- final normalizedChannelId = (channelId ?? "").trim();
- final currentRoomId =
- (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
- if (normalizedChannelId.isEmpty) {
- return false;
- }
- return normalizedChannelId == currentRoomId ||
- normalizedChannelId == (_joiningChannelId ?? "").trim();
- }
-
- void _cancelAgoraJoinWaiter([Object? error]) {
- _joinAgoraTimeoutTimer?.cancel();
- _joinAgoraTimeoutTimer = null;
- final completer = _joinAgoraCompleter;
- _joinAgoraCompleter = null;
- if (error != null && completer != null && !completer.isCompleted) {
- completer.completeError(error);
- }
- }
-
- void _cancelAgoraDisconnectedCleanup() {
- _agoraDisconnectedCleanupTimer?.cancel();
- _agoraDisconnectedCleanupTimer = null;
- }
-
- void _resetAgoraTracking({bool clearJoiningChannel = true}) {
- _cancelAgoraJoinWaiter(const _AgoraJoinCanceled('Agora join canceled'));
- _cancelAgoraDisconnectedCleanup();
- _agoraJoined = false;
- _agoraRoleIsBroadcaster = false;
- _agoraJoinedChannelId = null;
- if (clearJoiningChannel) {
- _joiningChannelId = null;
- }
+ void _resetRoomRtcTracking() {
+ _roomRtcJoined = false;
+ _roomRtcRoleIsBroadcaster = false;
_resetLocalAudioRuntimeTracking();
}
bool get _canReportMicActive {
- return _agoraJoined && _agoraRoleIsBroadcaster && isOnMai();
+ return _roomRtcJoined && _roomRtcRoleIsBroadcaster && isOnMai();
}
- bool get _joinAgoraCompleterIsPending {
- final completer = _joinAgoraCompleter;
- return completer != null && !completer.isCompleted;
- }
-
- void _resyncHeartbeatFromAgoraState() {
+ void _resyncHeartbeatFromRoomRtcState() {
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
final canReportMicActive = _canReportMicActive;
_syncRoomHeartbeatState(
@@ -1112,40 +1056,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
}
- bool _isFatalAgoraError(ErrorCodeType err) {
- return err == ErrorCodeType.errInvalidArgument ||
- err == ErrorCodeType.errNotReady ||
- err == ErrorCodeType.errNotSupported ||
- err == ErrorCodeType.errRefused ||
- err == ErrorCodeType.errInvalidState ||
- err == ErrorCodeType.errNoPermission ||
- err == ErrorCodeType.errJoinChannelRejected ||
- err == ErrorCodeType.errInvalidAppId ||
- err == ErrorCodeType.errInvalidChannelName ||
- err == ErrorCodeType.errTokenExpired ||
- err == ErrorCodeType.errInvalidToken ||
- err == ErrorCodeType.errConnectionLost;
- }
-
- bool _isFatalConnectionReason(ConnectionChangedReasonType reason) {
- return reason ==
- ConnectionChangedReasonType.connectionChangedBannedByServer ||
- reason == ConnectionChangedReasonType.connectionChangedJoinFailed ||
- reason == ConnectionChangedReasonType.connectionChangedInvalidAppId ||
- reason ==
- ConnectionChangedReasonType.connectionChangedInvalidChannelName ||
- reason == ConnectionChangedReasonType.connectionChangedInvalidToken ||
- reason == ConnectionChangedReasonType.connectionChangedTokenExpired ||
- reason ==
- ConnectionChangedReasonType.connectionChangedRejectedByServer ||
- reason == ConnectionChangedReasonType.connectionChangedSameUidLogin ||
- reason ==
- ConnectionChangedReasonType.connectionChangedTooManyBroadcasters ||
- reason ==
- ConnectionChangedReasonType
- .connectionChangedLicenseValidationFailure;
- }
-
MicRes? _currentUserMicSeat() {
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
@@ -1171,14 +1081,14 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (currentUserMic == null) {
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
- _agoraRoleIsBroadcaster = false;
+ _roomRtcRoleIsBroadcaster = false;
_syncRoomHeartbeatState(
roomId: roomId,
isOnMic: false,
shouldRunAnchorHeartbeat: false,
);
_applyLocalAudioRuntimeState(
- clientRole: ClientRoleType.clientRoleAudience,
+ role: _RoomRtcLocalAudioRole.audience,
muted: true,
);
_roomMusicMicRouteChangedListener?.call(false);
@@ -1194,9 +1104,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
_applyLocalAudioRuntimeState(
- clientRole: ClientRoleType.clientRoleBroadcaster,
+ role: _RoomRtcLocalAudioRole.anchor,
muted: (currentUserMic.micMute ?? false) || isMic,
- publisherToken: currentUserMic.roomToken,
);
_roomMusicMicRouteChangedListener?.call(true);
}
@@ -1212,7 +1121,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return;
}
- if (!_agoraJoined) {
+ if (!_roomRtcJoined) {
if (_lastScheduledVoiceLiveRoomId != null ||
_lastScheduledAnchorRoomId != null) {
SCHeartbeatUtils.scheduleHeartbeat(
@@ -1273,54 +1182,34 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
void _applyLocalAudioRuntimeState({
- required ClientRoleType clientRole,
+ required _RoomRtcLocalAudioRole role,
required bool muted,
- String? publisherToken,
}) {
- if (clientRole != ClientRoleType.clientRoleBroadcaster) {
- _agoraRoleIsBroadcaster = false;
+ if (role != _RoomRtcLocalAudioRole.anchor) {
+ _roomRtcRoleIsBroadcaster = false;
}
- if (engine == null) {
- _lastAppliedClientRole = null;
+ final adapter = _roomRtcEngineAdapter;
+ if (!_roomRtcJoined || adapter == null) {
+ _lastAppliedLocalAudioRole = null;
_lastAppliedLocalAudioMuted = null;
_lastAppliedRecordingSignalMuted = null;
_lastAppliedRecordingSignalVolume = null;
- _lastAppliedRtcToken = null;
return;
}
- final normalizedPublisherToken = (publisherToken ?? "").trim();
- if (clientRole == ClientRoleType.clientRoleBroadcaster &&
- normalizedPublisherToken.isNotEmpty &&
- _lastAppliedRtcToken != normalizedPublisherToken) {
- engine?.renewToken(normalizedPublisherToken);
- _lastAppliedRtcToken = normalizedPublisherToken;
+ final shouldPublishLocalAudio = role == _RoomRtcLocalAudioRole.anchor;
+ final recordingSignalVolume = shouldPublishLocalAudio && !muted ? 100 : 0;
+ if (_lastAppliedLocalAudioRole == role &&
+ _lastAppliedLocalAudioMuted == !shouldPublishLocalAudio &&
+ _lastAppliedRecordingSignalMuted == !shouldPublishLocalAudio &&
+ _lastAppliedRecordingSignalVolume == recordingSignalVolume) {
+ return;
}
- if (_lastAppliedClientRole != clientRole) {
- engine?.setClientRole(role: clientRole);
- _lastAppliedClientRole = clientRole;
- }
-
- final shouldPublishLocalAudio =
- clientRole == ClientRoleType.clientRoleBroadcaster;
- final localAudioMuted = !shouldPublishLocalAudio;
- if (_lastAppliedLocalAudioMuted != localAudioMuted) {
- engine?.muteLocalAudioStream(localAudioMuted);
- _lastAppliedLocalAudioMuted = localAudioMuted;
- }
-
- final recordingSignalMuted = !shouldPublishLocalAudio;
- if (_lastAppliedRecordingSignalMuted != recordingSignalMuted) {
- engine?.muteRecordingSignal(recordingSignalMuted);
- _lastAppliedRecordingSignalMuted = recordingSignalMuted;
- }
-
- final recordingSignalVolume =
- shouldPublishLocalAudio ? (muted ? 0 : 100) : 0;
- if (_lastAppliedRecordingSignalVolume != recordingSignalVolume) {
- adjustRecordingSignalVolume(recordingSignalVolume);
- _lastAppliedRecordingSignalVolume = recordingSignalVolume;
+ if (shouldPublishLocalAudio) {
+ unawaited(_switchRoomRtcToBroadcaster(muted: muted));
+ } else {
+ unawaited(_switchRoomRtcToAudience());
}
}
@@ -1331,11 +1220,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
void _resetLocalAudioRuntimeTracking() {
- _lastAppliedClientRole = null;
+ _lastAppliedLocalAudioRole = null;
_lastAppliedLocalAudioMuted = null;
_lastAppliedRecordingSignalMuted = null;
_lastAppliedRecordingSignalVolume = null;
- _lastAppliedRtcToken = null;
}
void _applyMicSnapshot(
@@ -1371,43 +1259,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return removedUserIds.isNotEmpty;
}
- int? _tryParseAgoraUid(String? value) {
- final normalizedValue = (value ?? '').trim();
- if (normalizedValue.isEmpty) {
- return null;
- }
- return int.tryParse(normalizedValue);
- }
-
- int _stableAgoraUidFromUser(SocialChatUserProfile? user) {
- final source =
- '${user?.account ?? ""}|${user?.id ?? ""}|${user?.userNickname ?? ""}';
- var hash = 0x811C9DC5;
- for (final codeUnit in source.codeUnits) {
- hash ^= codeUnit;
- hash = (hash * 0x01000193) & 0x7fffffff;
- }
- return hash == 0 ? 1 : hash;
- }
-
- int _resolveAgoraUidForUser(SocialChatUserProfile? user) {
- final parsedAccountUid = _tryParseAgoraUid(user?.account);
- if (parsedAccountUid != null && parsedAccountUid > 0) {
- return parsedAccountUid;
- }
- final parsedUserIdUid = _tryParseAgoraUid(user?.id);
- if (parsedUserIdUid != null && parsedUserIdUid > 0) {
- return parsedUserIdUid;
- }
- return _stableAgoraUidFromUser(user);
- }
-
- int _resolveAgoraUidForCurrentUser() {
- return _resolveAgoraUidForUser(
- AccountStorage().getCurrentUser()?.userProfile,
- );
- }
-
void requestGiftTriggeredMicRefresh() {
requestMicrophoneListRefresh(
notifyIfUnchanged: false,
@@ -1455,51 +1306,45 @@ class RealTimeCommunicationManager extends ChangeNotifier {
});
}
- Future _cleanupAgoraRoomState({
+ Future _cleanupRoomRtcState({
bool clearMicSeats = false,
- String? onlyIfJoinedChannelId,
+ String? onlyIfBusinessRoomId,
}) async {
- final expectedChannelId = (onlyIfJoinedChannelId ?? "").trim();
- if (expectedChannelId.isNotEmpty) {
- final joinedChannelId = (_agoraJoinedChannelId ?? "").trim();
+ final expectedRoomId = (onlyIfBusinessRoomId ?? "").trim();
+ if (expectedRoomId.isNotEmpty) {
final currentRoomId =
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
- if (joinedChannelId.isNotEmpty && joinedChannelId != expectedChannelId) {
- return;
- }
- if (joinedChannelId.isEmpty &&
- currentRoomId.isNotEmpty &&
- currentRoomId != expectedChannelId) {
+ if (currentRoomId.isNotEmpty && currentRoomId != expectedRoomId) {
return;
}
}
- _resetAgoraTracking();
+ _resetRoomRtcTracking();
try {
- await engine?.leaveChannel();
+ await _roomRtcEngineAdapter?.leaveRoom();
} catch (error) {}
if (clearMicSeats) {
roomWheatMap.clear();
notifyListeners();
}
- _resyncHeartbeatFromAgoraState();
+ _resyncHeartbeatFromRoomRtcState();
}
- Future _leaveAgoraRoomForExit() async {
- final rtcEngine = engine;
- _resetAgoraTracking();
- if (rtcEngine == null) {
+ Future _leaveRoomRtcForExit() async {
+ final adapter = _roomRtcEngineAdapter;
+ _resetRoomRtcTracking();
+ if (adapter == null) {
return;
}
try {
- await rtcEngine.leaveChannel();
+ await adapter.leaveRoom();
} catch (error) {}
}
- Future _handleAgoraJoinFailed(
+ Future _handleRoomRtcJoinFailed(
Object error, {
required bool exitRoom,
}) async {
- await _cleanupAgoraRoomState(clearMicSeats: true);
+ await _cleanupRoomRtcState(clearMicSeats: true);
if (!exitRoom || _isExitingCurrentVoiceRoomSession) {
return;
}
@@ -1507,61 +1352,54 @@ class RealTimeCommunicationManager extends ChangeNotifier {
unawaited(exitCurrentVoiceRoomSession(false));
}
- Future _handleAgoraDisconnected(Object reason) async {
- if (_isHandlingAgoraRoomFailure || _isExitingCurrentVoiceRoomSession) {
+ Future _handleRoomRtcDisconnected(Object reason) async {
+ if (_isHandlingRoomRtcFailure || _isExitingCurrentVoiceRoomSession) {
return;
}
- _isHandlingAgoraRoomFailure = true;
+ _isHandlingRoomRtcFailure = true;
SCTts.show("Voice connection failed");
- await _cleanupAgoraRoomState(clearMicSeats: true);
+ await _cleanupRoomRtcState(clearMicSeats: true);
await exitCurrentVoiceRoomSession(false);
}
- void _scheduleAgoraDisconnectedCleanup(Object reason) {
- if (_isExitingCurrentVoiceRoomSession) {
- return;
- }
- _agoraDisconnectedCleanupTimer?.cancel();
- _agoraDisconnectedCleanupTimer = Timer(_agoraDisconnectedGracePeriod, () {
- _agoraDisconnectedCleanupTimer = null;
- if (!_agoraJoined && !_isExitingCurrentVoiceRoomSession) {
- unawaited(_handleAgoraDisconnected(reason));
- }
- });
- }
-
Future prewarmRtcEngine() {
final existingTask = _rtcEnginePrewarmTask;
if (existingTask != null) {
return existingTask;
}
- final task = _prewarmRtcEngine();
+ final task = _currentRoomRtcEngineAdapter.prewarm();
_rtcEnginePrewarmTask = task;
+ unawaited(
+ task.whenComplete(() {
+ if (identical(_rtcEnginePrewarmTask, task)) {
+ _rtcEnginePrewarmTask = null;
+ }
+ }),
+ );
return task;
}
- Future _prewarmRtcEngine() async {
- try {
- final rtcEngine = await _initAgoraRtcEngine();
- await _configureAgoraAudioEngine(rtcEngine);
- } catch (error, stackTrace) {
- _rtcEnginePrewarmTask = null;
- rethrow;
- }
+ RoomRtcEngineAdapter get _currentRoomRtcEngineAdapter {
+ return _roomRtcEngineAdapter ??= TrtcRoomRtcEngineAdapter(
+ onVoiceVolume: _handleRoomRtcVoiceVolume,
+ onAudioMixingStateChanged: _handleRoomRtcAudioMixingStateChanged,
+ onRoomError: (error) => unawaited(_handleRoomRtcDisconnected(error)),
+ onRoomExit:
+ (reason) =>
+ unawaited(_handleRoomRtcDisconnected('TRTC exit: $reason')),
+ );
}
- Future _fetchRtcTokenForCurrentRoom({
- required bool isPublisher,
- }) async {
- final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
- final userId =
- (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
- final rtcToken = await SCAccountRepository().getRtcToken(
- channelId,
- userId,
- isPublisher: isPublisher,
- );
- return rtcToken.rtcToken ?? "";
+ String _resolveTrtcUserSigForCurrentUser() {
+ final userSig = (AccountStorage().getCurrentUser()?.userSig ?? "").trim();
+ if (userSig.isEmpty) {
+ throw StateError('TRTC userSig is empty');
+ }
+ return userSig;
+ }
+
+ Future _fetchRoomRtcJoinCredentialForCurrentRoom() async {
+ return _resolveTrtcUserSigForCurrentUser();
}
Future<_RoomStartupAsyncResult> _captureStartupFuture(
@@ -1574,129 +1412,94 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
}
- Future joinAgoraVoiceChannel({
+ Future joinRoomRtcVoiceChannel({
bool throwOnError = false,
- String? rtcToken,
+ required String trtcUserSig,
}) async {
- final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
- if (channelId.isEmpty) {
- final error = StateError('Agora channel id is empty');
- await _handleAgoraJoinFailed(error, exitRoom: !throwOnError);
+ final identity = RoomRtcSessionIdentity.fromJoinRoom(
+ roomProfile: currenRoom?.roomProfile?.roomProfile,
+ userProfile: AccountStorage().getCurrentUser()?.userProfile,
+ );
+ if (!identity.isValid || identity.businessRoomId.isEmpty) {
+ final error = StateError('TRTC room identity is invalid');
+ await _handleRoomRtcJoinFailed(error, exitRoom: !throwOnError);
if (throwOnError) {
throw error;
}
return;
}
- final pendingRoomSwitchLeaveTask = _pendingRoomSwitchAgoraLeaveTask;
+ final pendingRoomSwitchLeaveTask = _pendingRoomSwitchRtcLeaveTask;
if (pendingRoomSwitchLeaveTask != null) {
try {
await pendingRoomSwitchLeaveTask;
} catch (error) {}
}
- _cancelAgoraJoinWaiter(const _AgoraJoinCanceled('Agora join superseded'));
- _cancelAgoraDisconnectedCleanup();
- _joiningChannelId = channelId;
- _agoraJoined = false;
- _agoraRoleIsBroadcaster = false;
- final joinCompleter = Completer();
- _joinAgoraCompleter = joinCompleter;
- _joinAgoraTimeoutTimer = Timer(_agoraJoinTimeout, () {
- if (!joinCompleter.isCompleted) {
- joinCompleter.completeError(
- TimeoutException('Agora join timeout', _agoraJoinTimeout),
- );
- }
- });
+ _roomRtcJoined = false;
+ _roomRtcRoleIsBroadcaster = false;
try {
- final rtcEngine = await _initAgoraRtcEngine();
- await _configureAgoraAudioEngine(rtcEngine);
- final resolvedRtcToken =
- rtcToken ?? await _fetchRtcTokenForCurrentRoom(isPublisher: false);
- await rtcEngine.joinChannel(
- token: resolvedRtcToken,
- channelId: channelId,
- uid: _resolveAgoraUidForCurrentUser(),
- options: const ChannelMediaOptions(
- // 自动订阅所有视频流
- autoSubscribeVideo: false,
- // 自动订阅所有音频流
- autoSubscribeAudio: true,
- // 发布摄像头采集的视频
- publishCameraTrack: false,
- // 发布麦克风采集的音频
- publishMicrophoneTrack: true,
- // 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
- clientRoleType: ClientRoleType.clientRoleAudience,
- channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
+ final adapter = _currentRoomRtcEngineAdapter;
+ await adapter.enterRoomAsAudience(
+ RoomRtcEnterRoomConfig(
+ identity: identity,
+ trtcSdkAppId: SCGlobalConfig.trtcSdkAppId,
+ trtcUserSig: trtcUserSig,
+ throwOnError: true,
),
);
- await joinCompleter.future;
+ _roomRtcJoined = true;
+ _roomRtcRoleIsBroadcaster = false;
_syncSelfMicRuntimeState();
- rtcEngine.muteAllRemoteAudioStreams(roomIsMute);
+ adapter.muteAllRemoteAudio(roomIsMute);
} catch (e) {
- if (e is _AgoraJoinCanceled) {
- if (throwOnError) {
- rethrow;
- }
- return;
- }
- await _handleAgoraJoinFailed(e, exitRoom: !throwOnError);
+ await _handleRoomRtcJoinFailed(e, exitRoom: !throwOnError);
if (throwOnError) {
rethrow;
}
- } finally {
- if (identical(_joinAgoraCompleter, joinCompleter)) {
- _cancelAgoraJoinWaiter();
- }
}
}
- Future _switchAgoraToBroadcaster({
- required String publisherToken,
- required bool muted,
- }) async {
- if (!_agoraJoined || engine == null) {
+ Future _switchRoomRtcToBroadcaster({required bool muted}) async {
+ if (!_roomRtcJoined || _roomRtcEngineAdapter == null) {
return false;
}
try {
- if (publisherToken.trim().isNotEmpty) {
- await engine?.renewToken(publisherToken);
- _lastAppliedRtcToken = publisherToken.trim();
+ final switched = await _currentRoomRtcEngineAdapter.switchToAnchor(
+ RoomRtcAnchorConfig(muted: muted),
+ );
+ if (!switched) {
+ _roomRtcRoleIsBroadcaster = false;
+ _resyncHeartbeatFromRoomRtcState();
+ return false;
}
- await engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
- await engine?.muteLocalAudioStream(false);
- await engine?.muteRecordingSignal(false);
- await engine?.adjustRecordingSignalVolume(muted ? 0 : 100);
- _lastAppliedClientRole = ClientRoleType.clientRoleBroadcaster;
+ _lastAppliedLocalAudioRole = _RoomRtcLocalAudioRole.anchor;
_lastAppliedLocalAudioMuted = false;
_lastAppliedRecordingSignalMuted = false;
_lastAppliedRecordingSignalVolume = muted ? 0 : 100;
- _agoraRoleIsBroadcaster = true;
- _resyncHeartbeatFromAgoraState();
+ _roomRtcRoleIsBroadcaster = true;
+ _resyncHeartbeatFromRoomRtcState();
return true;
} catch (error) {
- _agoraRoleIsBroadcaster = false;
- _resyncHeartbeatFromAgoraState();
+ _roomRtcRoleIsBroadcaster = false;
+ _resyncHeartbeatFromRoomRtcState();
return false;
}
}
- Future _switchAgoraToAudience() async {
+ Future _switchRoomRtcToAudience() async {
try {
- await engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
- await engine?.muteLocalAudioStream(true);
- await engine?.muteRecordingSignal(true);
- await engine?.adjustRecordingSignalVolume(0);
- _lastAppliedClientRole = ClientRoleType.clientRoleAudience;
+ final switched = await _currentRoomRtcEngineAdapter.switchToAudience();
+ if (!switched) {
+ return false;
+ }
+ _lastAppliedLocalAudioRole = _RoomRtcLocalAudioRole.audience;
_lastAppliedLocalAudioMuted = true;
_lastAppliedRecordingSignalMuted = true;
_lastAppliedRecordingSignalVolume = 0;
- _lastAppliedRtcToken = null;
- _agoraRoleIsBroadcaster = false;
- _resyncHeartbeatFromAgoraState();
+ _roomRtcRoleIsBroadcaster = false;
+ _resyncHeartbeatFromRoomRtcState();
return true;
} catch (error) {
return false;
@@ -1716,17 +1519,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final seat = roomWheatMap[seatIndex];
final seatMuted = seat?.micMute ?? false;
if (seatMuted || muted) {
- if (_agoraJoined && isOnMai()) {
- await _switchAgoraToBroadcaster(
- publisherToken: seat?.roomToken ?? "",
- muted: true,
- );
+ if (_roomRtcJoined && isOnMai()) {
+ await _switchRoomRtcToBroadcaster(muted: true);
}
} else {
- await _switchAgoraToBroadcaster(
- publisherToken: seat?.roomToken ?? "",
- muted: false,
- );
+ await _switchRoomRtcToBroadcaster(muted: false);
}
notifyListeners();
}
@@ -1737,7 +1534,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (currentUserId.isEmpty) {
- await _switchAgoraToAudience();
+ await _switchRoomRtcToAudience();
return;
}
@@ -1750,11 +1547,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_clearSelfMicReleaseGuard();
}
_clearUserFromSeats(currentUserId);
- _agoraRoleIsBroadcaster = false;
+ _roomRtcRoleIsBroadcaster = false;
_syncSelfMicRuntimeState();
notifyListeners();
- await _switchAgoraToAudience();
+ await _switchRoomRtcToAudience();
if (refreshMicList) {
requestMicrophoneListRefresh(
notifyIfUnchanged: false,
@@ -1763,248 +1560,22 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
}
- Future _initAgoraRtcEngine() {
- if (engine != null) {
- return Future.value(engine!);
- }
- final existingTask = _rtcEngineInitTask;
- if (existingTask != null) {
- return existingTask;
- }
- final task = _createAgoraRtcEngine();
- _rtcEngineInitTask = task;
- void clearInitTask() {
- if (identical(_rtcEngineInitTask, task)) {
- _rtcEngineInitTask = null;
- }
- }
-
- unawaited(
- task.then(
- (_) => clearInitTask(),
- onError: (_, __) => clearInitTask(),
- ),
- );
- return task;
- }
-
- Future _createAgoraRtcEngine() async {
- final rtcEngine = createAgoraRtcEngine();
- await rtcEngine.initialize(
- RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid),
- );
- engine = rtcEngine;
- _rtcEngineEventHandler = RtcEngineEventHandler(
- onError: (ErrorCodeType err, String msg) {
- final error = 'Agora error: $err $msg';
- if (!_agoraJoined) {
- final completer = _joinAgoraCompleter;
- if (completer != null && !completer.isCompleted) {
- completer.completeError(StateError(error));
- } else {
- unawaited(_handleAgoraJoinFailed(error, exitRoom: true));
- }
- return;
- }
- if (_isFatalAgoraError(err)) {
- unawaited(_handleAgoraDisconnected(error));
- }
- },
- onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
- final joinedChannelId = connection.channelId ?? "";
- if (!_isCurrentAgoraChannel(joinedChannelId)) {
- return;
- }
- _agoraJoined = true;
- _agoraRoleIsBroadcaster = false;
- _agoraJoinedChannelId = joinedChannelId.trim();
- _cancelAgoraDisconnectedCleanup();
- _joinAgoraTimeoutTimer?.cancel();
- _joinAgoraTimeoutTimer = null;
- final completer = _joinAgoraCompleter;
- if (completer != null && !completer.isCompleted) {
- completer.complete();
- }
- _resyncHeartbeatFromAgoraState();
- },
- onLeaveChannel: (RtcConnection connection, RtcStats stats) {
- if (_isCurrentAgoraChannel(connection.channelId)) {
- final completer = _joinAgoraCompleter;
- if (completer != null && !completer.isCompleted) {
- completer.completeError(
- StateError('Agora left channel before join success'),
- );
- }
- _resetAgoraTracking();
- _resyncHeartbeatFromAgoraState();
- }
- },
- onClientRoleChanged: (
- RtcConnection connection,
- ClientRoleType oldRole,
- ClientRoleType newRole,
- ClientRoleOptions newRoleOptions,
- ) {
- if (!_isCurrentAgoraChannel(connection.channelId)) {
- return;
- }
- _agoraRoleIsBroadcaster =
- newRole == ClientRoleType.clientRoleBroadcaster;
- _resyncHeartbeatFromAgoraState();
- },
- onClientRoleChangeFailed: (
- RtcConnection connection,
- ClientRoleChangeFailedReason reason,
- ClientRoleType currentRole,
- ) {
- if (!_isCurrentAgoraChannel(connection.channelId)) {
- return;
- }
- _agoraRoleIsBroadcaster =
- currentRole == ClientRoleType.clientRoleBroadcaster;
- _resyncHeartbeatFromAgoraState();
- },
- onConnectionStateChanged: (
- RtcConnection connection,
- ConnectionStateType state,
- ConnectionChangedReasonType reason,
- ) {
- if (!_isCurrentAgoraChannel(connection.channelId)) {
- return;
- }
- if (state == ConnectionStateType.connectionStateConnected &&
- reason ==
- ConnectionChangedReasonType.connectionChangedRejoinSuccess) {
- _agoraJoined = true;
- _agoraJoinedChannelId = connection.channelId?.trim();
- _cancelAgoraDisconnectedCleanup();
- _resyncHeartbeatFromAgoraState();
- return;
- }
- if (reason ==
- ConnectionChangedReasonType.connectionChangedLeaveChannel) {
- _resetAgoraTracking();
- _resyncHeartbeatFromAgoraState();
- return;
- }
- if (state == ConnectionStateType.connectionStateFailed ||
- _isFatalConnectionReason(reason)) {
- _agoraJoined = false;
- _agoraRoleIsBroadcaster = false;
- _resyncHeartbeatFromAgoraState();
- if (_joinAgoraCompleterIsPending) {
- _joinAgoraCompleter?.completeError(
- StateError('Agora connection failed before join: $reason'),
- );
- } else {
- unawaited(
- _handleAgoraDisconnected('Agora connection failed: $reason'),
- );
- }
- return;
- }
- if (state == ConnectionStateType.connectionStateDisconnected ||
- state == ConnectionStateType.connectionStateReconnecting) {
- _agoraJoined = false;
- _agoraRoleIsBroadcaster = false;
- _resyncHeartbeatFromAgoraState();
- _scheduleAgoraDisconnectedCleanup(reason);
- }
- },
- onAudioMixingStateChanged: (
- AudioMixingStateType state,
- AudioMixingReasonType reason,
- ) {
- switch (state) {
- case AudioMixingStateType.audioMixingStatePlaying:
- isMusicPlaying = true;
- break;
- case AudioMixingStateType.audioMixingStateStopped:
- case AudioMixingStateType.audioMixingStateFailed:
- isMusicPlaying = false;
- break;
- default:
- break;
- }
- _roomMusicMixingStateListener?.call(state, reason);
- },
- onUserJoined: (connection, remoteUid, elapsed) {},
- // 监听远端用户离开
- onUserOffline: (connection, remoteUid, reason) {},
- onTokenPrivilegeWillExpire: (
- RtcConnection connection,
- String token,
- ) async {
- var rtcToken = await SCAccountRepository().getRtcToken(
- currenRoom?.roomProfile?.roomProfile?.id ?? "",
- AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
- isPublisher: _agoraRoleIsBroadcaster,
- );
- engine?.renewToken(rtcToken.rtcToken ?? "");
- },
- onAudioVolumeIndication: initializeAudioVolumeIndicationCallback,
- );
- rtcEngine.registerEventHandler(_rtcEngineEventHandler!);
- await _configureAgoraAudioEngine(rtcEngine);
- return rtcEngine;
- }
-
- Future _configureAgoraAudioEngine(RtcEngine rtcEngine) async {
- if (_agoraAudioConfigured) {
- return;
- }
- await rtcEngine.setAudioProfile(
- profile: AudioProfileType.audioProfileSpeechStandard,
- scenario: AudioScenarioType.audioScenarioGameStreaming,
- );
- await rtcEngine.enableAudioVolumeIndication(
- interval: 500,
- smooth: 3,
- reportVad: true,
- );
- await rtcEngine.disableVideo();
- _agoraAudioConfigured = true;
- }
-
Future releaseRtcEngineForAppTermination() async {
- final rtcEngine = engine;
- if (rtcEngine == null) {
- await SCVoiceRoomForegroundService.stop();
- return;
- }
-
- final eventHandler = _rtcEngineEventHandler;
- engine = null;
- _rtcEngineEventHandler = null;
- _rtcEngineInitTask = null;
+ final adapter = _roomRtcEngineAdapter;
+ _roomRtcEngineAdapter = null;
_rtcEnginePrewarmTask = null;
- _agoraAudioConfigured = false;
-
+ _resetRoomRtcTracking();
try {
- if (eventHandler != null) {
- rtcEngine.unregisterEventHandler(eventHandler);
- }
- await rtcEngine.leaveChannel();
- } catch (e) {}
- _resetAgoraTracking();
-
- try {
- await rtcEngine.release();
+ await adapter?.release();
} catch (e) {}
await SCVoiceRoomForegroundService.stop();
}
- void initializeAudioVolumeIndicationCallback(
- RtcConnection connection,
- List speakers,
- int speakerNumber,
+ void _handleRoomRtcVoiceVolume(
+ List userVolumes,
int totalVolume,
) {
- // if (user == null) {
- // throw "本地User尚未初始化";
- // }
-
- if (roomWheatMap.isEmpty || speakers.isEmpty) {
+ if (roomWheatMap.isEmpty || userVolumes.isEmpty) {
return;
}
@@ -2012,25 +1583,23 @@ class RealTimeCommunicationManager extends ChangeNotifier {
seat.setVolume = 0;
}
- final seatIndexByAgoraUid = {};
+ final seatIndexByUserId = {};
roomWheatMap.forEach((seatIndex, mic) {
- final user = mic.user;
- if (user == null) {
+ final userId = (mic.user?.id ?? "").trim();
+ if (userId.isEmpty) {
return;
}
- seatIndexByAgoraUid[_resolveAgoraUidForUser(user)] = seatIndex;
+ seatIndexByUserId[userId] = seatIndex;
});
- final currentUserAgoraUid = _resolveAgoraUidForCurrentUser();
final listeners = List.from(_onSoundVoiceChangeList);
- for (final info in speakers) {
- final volume = info.volume ?? 0;
+ for (final info in userVolumes) {
+ final volume = info.volume;
if (volume <= 0) {
continue;
}
- final resolvedAgoraUid = info.uid == 0 ? currentUserAgoraUid : info.uid;
- final seatIndex = seatIndexByAgoraUid[resolvedAgoraUid];
+ final seatIndex = seatIndexByUserId[info.userId.trim()];
if (seatIndex == null) {
continue;
}
@@ -2045,6 +1614,24 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
}
+ void _handleRoomRtcAudioMixingStateChanged(
+ RoomRtcAudioMixingState state,
+ RoomRtcAudioMixingReason reason,
+ ) {
+ switch (state) {
+ case RoomRtcAudioMixingState.playing:
+ isMusicPlaying = true;
+ break;
+ case RoomRtcAudioMixingState.stopped:
+ case RoomRtcAudioMixingState.failed:
+ isMusicPlaying = false;
+ break;
+ case RoomRtcAudioMixingState.paused:
+ break;
+ }
+ _roomMusicMixingStateListener?.call(state, reason);
+ }
+
///关注/取消房间
void followCurrentVoiceRoom() async {
var result = await SCAccountRepository().followRoom(
@@ -2854,8 +2441,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
try {
final joinGroupFuture = rtmProvider?.joinRoomGroup(groupId, "");
- final rtcTokenFuture = _captureStartupFuture(
- _fetchRtcTokenForCurrentRoom(isPublisher: false),
+ final roomRtcCredentialFuture = _captureStartupFuture(
+ _fetchRoomRtcJoinCredentialForCurrentRoom(),
);
final microphoneListFuture = retrieveMicrophoneList();
final onlineUsersFuture = fetchOnlineUsersList();
@@ -2895,16 +2482,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
socialChatRoomManager.fetchContributionLevelData(roomId);
failureType = RoomStartupFailureType.rtc;
- final rtcTokenResult = await rtcTokenFuture;
+ final roomRtcCredentialResult = await roomRtcCredentialFuture;
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
return;
}
- final rtcToken = rtcTokenResult.requireValue();
- await joinAgoraVoiceChannel(throwOnError: true, rtcToken: rtcToken);
+ final trtcUserSig = roomRtcCredentialResult.requireValue();
+ await joinRoomRtcVoiceChannel(
+ throwOnError: true,
+ trtcUserSig: trtcUserSig,
+ );
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
- await _cleanupAgoraRoomState(
+ await _cleanupRoomRtcState(
clearMicSeats: currenRoom == null,
- onlyIfJoinedChannelId: roomId,
+ onlyIfBusinessRoomId: roomId,
);
return;
}
@@ -3481,7 +3071,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
VoiceRoomRoute.popVoiceRoomToPrevious(navigationContext);
}
- final agoraLeaveTask = _leaveAgoraRoomForExit();
+ final roomRtcLeaveTask = _leaveRoomRtcForExit();
final remoteCleanupTask = _cleanupExitedRoomRemotely(
exitRtmProvider: exitRtmProvider,
groupId: isPreviewOnlyExit ? "" : groupId,
@@ -3489,10 +3079,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
shouldSendOnlineHeartbeat: !isLogout,
);
if (deferRemoteCleanup) {
- unawaited(agoraLeaveTask);
+ unawaited(roomRtcLeaveTask);
unawaited(remoteCleanupTask);
} else {
- await agoraLeaveTask;
+ await roomRtcLeaveTask;
await remoteCleanupTask;
}
}
@@ -3515,12 +3105,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} catch (e) {}
_clearData();
- final agoraLeaveTask = _leaveAgoraRoomForExit();
- _pendingRoomSwitchAgoraLeaveTask = agoraLeaveTask;
+ final roomRtcLeaveTask = _leaveRoomRtcForExit();
+ _pendingRoomSwitchRtcLeaveTask = roomRtcLeaveTask;
unawaited(
- agoraLeaveTask.whenComplete(() {
- if (identical(_pendingRoomSwitchAgoraLeaveTask, agoraLeaveTask)) {
- _pendingRoomSwitchAgoraLeaveTask = null;
+ roomRtcLeaveTask.whenComplete(() {
+ if (identical(_pendingRoomSwitchRtcLeaveTask, roomRtcLeaveTask)) {
+ _pendingRoomSwitchRtcLeaveTask = null;
}
}),
);
@@ -3629,7 +3219,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (groupId.isNotEmpty) {
await rtmProvider?.quitGroup(groupId);
}
- await _cleanupAgoraRoomState();
+ await _cleanupRoomRtcState();
await Future.delayed(const Duration(milliseconds: 100));
} catch (e) {
} finally {
@@ -3663,7 +3253,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (!isPreviewOnlyFailure && groupId.isNotEmpty) {
await rtmProvider?.quitGroup(groupId);
}
- await _cleanupAgoraRoomState(clearMicSeats: true);
+ await _cleanupRoomRtcState(clearMicSeats: true);
await Future.delayed(const Duration(milliseconds: 100));
if (!isPreviewOnlyFailure && roomId.isNotEmpty) {
await SCChatRoomRepository().roomRedPacketPresenceLeave(roomId);
@@ -3695,7 +3285,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_clearRoomContributionLevelData();
_stopRoomStatePolling();
_resetHeartbeatTracking();
- _resetAgoraTracking();
+ _resetRoomRtcTracking();
_disableMicListRefreshForCurrentSession = false;
_disableOnlineUsersRefreshForCurrentSession = false;
_isSelfMicActionInFlight = false;
@@ -3703,7 +3293,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomStartupFailureType = RoomStartupFailureType.none;
_currentRoomIsEntryPreview = false;
_isHandlingRoomStartupFailure = false;
- _isHandlingAgoraRoomFailure = false;
+ _isHandlingRoomRtcFailure = false;
_pendingMicListRefresh = false;
_pendingMicListRefreshNotifyIfUnchanged = false;
_seatEmojiEventVersions.clear();
@@ -3751,7 +3341,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
void toggleRemoteAudioMuteForAllUsers() {
roomIsMute = !roomIsMute;
- engine?.muteAllRemoteAudioStreams(roomIsMute);
+ _roomRtcEngineAdapter?.muteAllRemoteAudio(roomIsMute);
notifyListeners();
}
@@ -3943,12 +3533,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
final targetIndex = micGoUpRes.micIndex ?? index;
- final switched = await _switchAgoraToBroadcaster(
- publisherToken: micGoUpRes.roomToken ?? "",
+ final switched = await _switchRoomRtcToBroadcaster(
muted: (micGoUpRes.micMute ?? false) || isMic,
);
if (!switched) {
- await _switchAgoraToAudience();
+ await _switchRoomRtcToAudience();
try {
await SCChatRoomRepository().micGoDown(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
@@ -3961,7 +3550,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
notifyListeners();
_refreshMicListSilently(notifyIfUnchanged: true);
_handleSelfMicGoUpFailure(
- 'Failed to put on the microphone, Agora switch failed',
+ 'Failed to put on the microphone, TRTC switch failed',
);
return;
}
@@ -4050,7 +3639,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
SCHeartbeatUtils.cancelAnchorTimer();
- await _switchAgoraToAudience();
+ await _switchRoomRtcToAudience();
_clearUserFromSeats(currentUserId);
_syncSelfMicRuntimeState();
notifyListeners();
@@ -4282,10 +3871,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
true,
);
if (isOnMaiInIndex(index)) {
- await _switchAgoraToBroadcaster(
- publisherToken: roomWheatMap[index]?.roomToken ?? "",
- muted: true,
- );
+ await _switchRoomRtcToBroadcaster(muted: true);
}
var mic = roomWheatMap[index];
if (mic != null) {
@@ -4305,10 +3891,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (isOnMaiInIndex(index)) {
if (!isMic) {
if (roomWheatMap[index]?.user != null) {
- await _switchAgoraToBroadcaster(
- publisherToken: roomWheatMap[index]?.roomToken ?? "",
- muted: false,
- );
+ await _switchRoomRtcToBroadcaster(muted: false);
}
}
}
@@ -4419,17 +4002,15 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (localPath.isEmpty) {
return;
}
- engine?.startAudioMixing(
- filePath: localPath,
- loopback: false,
- cycle: cycle,
+ unawaited(
+ startRoomMusicAudioMixing(
+ filePath: localPath,
+ loopback: false,
+ cycle: cycle,
+ ).catchError((_) {}),
);
}
- void adjustRecordingSignalVolume(int volume) {
- engine?.adjustRecordingSignalVolume(volume);
- }
-
void setRoomMusicMixingStateListener(RoomMusicMixingStateListener? listener) {
_roomMusicMixingStateListener = listener;
}
@@ -4549,54 +4130,54 @@ class RealTimeCommunicationManager extends ChangeNotifier {
throw StateError('Current user is not ready to publish room music');
}
}
- await engine?.startAudioMixing(
- filePath: filePath,
- loopback: loopback,
- cycle: cycle,
- startPos: startPos,
+ await _currentRoomRtcEngineAdapter.startAudioMixing(
+ RoomRtcAudioMixingConfig(
+ filePath: filePath,
+ loopback: loopback,
+ cycle: cycle,
+ startPos: startPos,
+ ),
);
}
Future pauseRoomMusicAudioMixing() async {
- await engine?.pauseAudioMixing();
+ await _roomRtcEngineAdapter?.pauseAudioMixing();
}
Future resumeRoomMusicAudioMixing() async {
- await engine?.resumeAudioMixing();
+ await _roomRtcEngineAdapter?.resumeAudioMixing();
}
Future stopRoomMusicAudioMixing() async {
- await engine?.stopAudioMixing();
+ await _roomRtcEngineAdapter?.stopAudioMixing();
}
Future setRoomMusicAudioMixingPosition(int positionMs) async {
- await engine?.setAudioMixingPosition(positionMs);
+ await _roomRtcEngineAdapter?.setAudioMixingPosition(positionMs);
}
Future getRoomMusicAudioMixingPosition() async {
- return await engine?.getAudioMixingCurrentPosition() ?? 0;
+ return await _roomRtcEngineAdapter?.getAudioMixingPosition() ?? 0;
}
Future getRoomMusicAudioMixingDuration() async {
- return await engine?.getAudioMixingDuration() ?? 0;
+ return await _roomRtcEngineAdapter?.getAudioMixingDuration() ?? 0;
}
Future setRoomMusicAudioMixingVolume(int volume) async {
final safeVolume = volume.clamp(0, 100).toInt();
- await engine?.adjustAudioMixingPlayoutVolume(safeVolume);
- await engine?.adjustAudioMixingPublishVolume(safeVolume);
+ await _roomRtcEngineAdapter?.setAudioMixingVolume(safeVolume);
}
Future _ensureRoomMusicPublishReady() async {
- if (!_agoraJoined || engine == null) {
+ if (!_roomRtcJoined || _roomRtcEngineAdapter == null) {
return false;
}
final currentUserMic = _currentUserMicSeat();
if (currentUserMic == null) {
return false;
}
- return _switchAgoraToBroadcaster(
- publisherToken: currentUserMic.roomToken ?? "",
+ return _switchRoomRtcToBroadcaster(
muted: (currentUserMic.micMute ?? false) || isMic,
);
}
diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart
index 6770751..47ae4f9 100644
--- a/lib/services/audio/rtm_manager.dart
+++ b/lib/services/audio/rtm_manager.dart
@@ -1746,6 +1746,27 @@ class RealTimeMessagingManager extends ChangeNotifier {
currentRoomId == normalizedRoomId;
}
+ bool _isCurrentHiddenVoiceRoom(String roomId) {
+ final normalizedRoomId = roomId.trim();
+ final currentContext = context;
+ if (normalizedRoomId.isEmpty ||
+ currentContext == null ||
+ !currentContext.mounted) {
+ return false;
+ }
+ final rtcProvider = Provider.of(
+ currentContext,
+ listen: false,
+ );
+ final currentRoomId =
+ rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
+ if (currentRoomId.isEmpty || currentRoomId != normalizedRoomId) {
+ return false;
+ }
+ return !rtcProvider.shouldShowRoomVisualEffects ||
+ !rtcProvider.isVoiceRoomRouteVisible;
+ }
+
String _currentVoiceRoomId() {
final currentContext = context;
if (currentContext == null || !currentContext.mounted) {
@@ -1854,6 +1875,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
return;
}
+ var isCurrentRoom = false;
+ var isCurrentVisibleRoom = false;
final currentContext = context;
if (currentContext != null && currentContext.mounted) {
final rtcProvider = Provider.of(
@@ -1863,10 +1886,19 @@ class RealTimeMessagingManager extends ChangeNotifier {
final currentRoomId =
(rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim();
if (currentRoomId == launch.roomId) {
- rtcProvider.handleRoomRocketLaunchBroadcast(launch);
+ isCurrentRoom = true;
+ isCurrentVisibleRoom =
+ rtcProvider.shouldShowRoomVisualEffects &&
+ rtcProvider.isVoiceRoomRouteVisible;
+ if (isCurrentVisibleRoom) {
+ rtcProvider.handleRoomRocketLaunchBroadcast(launch);
+ }
}
}
+ if (isCurrentRoom && !isCurrentVisibleRoom) {
+ return;
+ }
OverlayManager().addMessage(_buildRoomRocketLaunchFloatingMessage(launch));
}
@@ -1879,6 +1911,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
(currentRoomId.isNotEmpty && resolvedRoomId != currentRoomId)) {
return;
}
+ if (!_isCurrentVoiceRoom(resolvedRoomId)) {
+ return;
+ }
final userIds = _payloadStringList(data['userIds']);
final currentUserId =
@@ -2010,6 +2045,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
),
role: _rocketRewardRole(record.rewardType, record.rewardCover),
number: record.level <= 0 ? 1 : record.level,
+ rocketIconUrl: _firstNonBlank([
+ record.rocketIconUrl,
+ _rocketIconUrlForLevel(record.level),
+ ]),
giftBatchId: _rocketRewardRecordKey(record),
);
}
@@ -2039,6 +2078,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
user: _rocketRewardUserFromPayload(payload, userId),
role: _rocketRewardRole(rewardType, rewardCover),
number: level <= 0 ? 1 : level,
+ rocketIconUrl: _rocketRewardRocketIconUrl(payload, level),
giftBatchId: [
'rocket_reward_fallback',
_payloadText(payload['launchNo']),
@@ -2116,6 +2156,46 @@ class RealTimeMessagingManager extends ChangeNotifier {
return rewardCover.trim().isEmpty ? 'GOLD' : normalized;
}
+ String _rocketRewardRocketIconUrl(Map payload, int level) {
+ final rocket = _broadcastPayloadMap(payload['rocket']);
+ final levelInfo = _broadcastPayloadMap(payload['levelInfo']);
+ final config = _broadcastPayloadMap(payload['config']);
+ return _firstNonBlank([
+ _payloadText(payload['rocket_icon_url']),
+ _payloadText(payload['rocketIconUrl']),
+ _payloadText(payload['rocketIcon']),
+ _payloadText(payload['iconUrl']),
+ _payloadText(rocket['rocket_icon_url']),
+ _payloadText(rocket['rocketIconUrl']),
+ _payloadText(rocket['rocketIcon']),
+ _payloadText(levelInfo['rocket_icon_url']),
+ _payloadText(levelInfo['rocketIconUrl']),
+ _payloadText(levelInfo['rocketIcon']),
+ _payloadText(config['rocket_icon_url']),
+ _payloadText(config['rocketIconUrl']),
+ _payloadText(config['rocketIcon']),
+ _rocketIconUrlForLevel(level),
+ ]);
+ }
+
+ String _rocketIconUrlForLevel(int level) {
+ final currentContext = context;
+ if (currentContext == null || !currentContext.mounted) {
+ return '';
+ }
+ final rtcProvider = Provider.of(
+ currentContext,
+ listen: false,
+ );
+ final levels = rtcProvider.roomRocketStatus?.levels ?? const [];
+ for (final item in levels) {
+ if (item.level == level && item.rocketIconUrl.trim().isNotEmpty) {
+ return item.rocketIconUrl;
+ }
+ }
+ return '';
+ }
+
String _rocketRewardRecordKey(SCRoomRocketRewardRecordRes record) {
return [
'rocket_reward',
@@ -2360,6 +2440,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
} else if (type == "ROCKET_ENERGY_LAUNCH") {
///火箭触发飘屏
var fdata = data["data"];
+ final launchRoomId = _payloadText(fdata["roomId"]);
+ if (_isCurrentHiddenVoiceRoom(launchRoomId)) {
+ return;
+ }
final rocketIconUrl = _firstNonBlank([
_payloadText(fdata["rocketIconUrl"]),
_payloadText(fdata["rocketIcon"]),
@@ -2370,7 +2454,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
]);
SCFloatingMessage msg = SCFloatingMessage(
type: 3,
- roomId: fdata["roomId"],
+ roomId: launchRoomId,
rocketLevel: fdata["fromLevel"],
userAvatarUrl: _firstNonBlank([
_payloadText(fdata["roomCoverUrl"]),
diff --git a/lib/services/audio/trtc_room_rtc_engine_adapter.dart b/lib/services/audio/trtc_room_rtc_engine_adapter.dart
new file mode 100644
index 0000000..765d2ae
--- /dev/null
+++ b/lib/services/audio/trtc_room_rtc_engine_adapter.dart
@@ -0,0 +1,590 @@
+import 'dart:async';
+
+import 'package:tencent_rtc_sdk/trtc_cloud.dart';
+import 'package:tencent_rtc_sdk/trtc_cloud_def.dart';
+import 'package:tencent_rtc_sdk/trtc_cloud_listener.dart';
+import 'package:tencent_rtc_sdk/tx_audio_effect_manager.dart';
+
+import 'room_rtc_engine_adapter.dart';
+import 'room_rtc_types.dart';
+
+abstract class RoomTrtcCloud {
+ void registerListener(TRTCCloudListener listener);
+ void unRegisterListener(TRTCCloudListener listener);
+ void setDefaultStreamRecvMode(bool autoRecvAudio, bool autoRecvVideo);
+ void enableAudioVolumeEvaluation(
+ bool enable,
+ TRTCAudioVolumeEvaluateParams params,
+ );
+ void enterRoom(TRTCParams param, TRTCAppScene scene);
+ void exitRoom();
+ void switchRole(TRTCRoleType role);
+ void startLocalAudio(TRTCAudioQuality quality);
+ void stopLocalAudio();
+ void muteLocalAudio(bool mute);
+ void muteAllRemoteAudio(bool mute);
+ RoomTrtcAudioEffectManager getAudioEffectManager();
+}
+
+abstract class RoomTrtcAudioEffectManager {
+ void setMusicObserver(int musicId, TXMusicPlayObserver? observer);
+ void startPlayMusic(AudioMusicParam musicParam);
+ void stopPlayMusic(int id);
+ void pausePlayMusic(int id);
+ void resumePlayMusic(int id);
+ void setMusicPublishVolume(int id, int volume);
+ void setMusicPlayoutVolume(int id, int volume);
+ int getMusicCurrentPosInMS(int id);
+ int getMusicDurationInMS(String path);
+ void seekMusicToPosInTime(int id, int pts);
+}
+
+class TencentRoomTrtcCloud implements RoomTrtcCloud {
+ TencentRoomTrtcCloud._(this._cloud);
+
+ static Future sharedInstance() async {
+ return TencentRoomTrtcCloud._(await TRTCCloud.sharedInstance());
+ }
+
+ static void destroySharedInstance() {
+ TRTCCloud.destroySharedInstance();
+ }
+
+ final TRTCCloud _cloud;
+
+ @override
+ void registerListener(TRTCCloudListener listener) {
+ _cloud.registerListener(listener);
+ }
+
+ @override
+ void unRegisterListener(TRTCCloudListener listener) {
+ _cloud.unRegisterListener(listener);
+ }
+
+ @override
+ void setDefaultStreamRecvMode(bool autoRecvAudio, bool autoRecvVideo) {
+ _cloud.setDefaultStreamRecvMode(autoRecvAudio, autoRecvVideo);
+ }
+
+ @override
+ void enableAudioVolumeEvaluation(
+ bool enable,
+ TRTCAudioVolumeEvaluateParams params,
+ ) {
+ _cloud.enableAudioVolumeEvaluation(enable, params);
+ }
+
+ @override
+ void enterRoom(TRTCParams param, TRTCAppScene scene) {
+ _cloud.enterRoom(param, scene);
+ }
+
+ @override
+ void exitRoom() {
+ _cloud.exitRoom();
+ }
+
+ @override
+ void switchRole(TRTCRoleType role) {
+ _cloud.switchRole(role);
+ }
+
+ @override
+ void startLocalAudio(TRTCAudioQuality quality) {
+ _cloud.startLocalAudio(quality);
+ }
+
+ @override
+ void stopLocalAudio() {
+ _cloud.stopLocalAudio();
+ }
+
+ @override
+ void muteLocalAudio(bool mute) {
+ _cloud.muteLocalAudio(mute);
+ }
+
+ @override
+ void muteAllRemoteAudio(bool mute) {
+ _cloud.muteAllRemoteAudio(mute);
+ }
+
+ @override
+ RoomTrtcAudioEffectManager getAudioEffectManager() {
+ return TencentRoomTrtcAudioEffectManager(_cloud.getAudioEffectManager());
+ }
+}
+
+class TencentRoomTrtcAudioEffectManager implements RoomTrtcAudioEffectManager {
+ const TencentRoomTrtcAudioEffectManager(this._manager);
+
+ final TXAudioEffectManager _manager;
+
+ @override
+ void setMusicObserver(int musicId, TXMusicPlayObserver? observer) {
+ _manager.setMusicObserver(musicId, observer);
+ }
+
+ @override
+ void startPlayMusic(AudioMusicParam musicParam) {
+ _manager.startPlayMusic(musicParam);
+ }
+
+ @override
+ void stopPlayMusic(int id) {
+ _manager.stopPlayMusic(id);
+ }
+
+ @override
+ void pausePlayMusic(int id) {
+ _manager.pausePlayMusic(id);
+ }
+
+ @override
+ void resumePlayMusic(int id) {
+ _manager.resumePlayMusic(id);
+ }
+
+ @override
+ void setMusicPublishVolume(int id, int volume) {
+ _manager.setMusicPublishVolume(id, volume);
+ }
+
+ @override
+ void setMusicPlayoutVolume(int id, int volume) {
+ _manager.setMusicPlayoutVolume(id, volume);
+ }
+
+ @override
+ int getMusicCurrentPosInMS(int id) {
+ return _manager.getMusicCurrentPosInMS(id);
+ }
+
+ @override
+ int getMusicDurationInMS(String path) {
+ return _manager.getMusicDurationInMS(path);
+ }
+
+ @override
+ void seekMusicToPosInTime(int id, int pts) {
+ _manager.seekMusicToPosInTime(id, pts);
+ }
+}
+
+class TrtcRoomRtcEngineAdapter implements RoomRtcEngineAdapter {
+ TrtcRoomRtcEngineAdapter({
+ Future Function()? cloudFactory,
+ this.onVoiceVolume,
+ this.onAudioMixingStateChanged,
+ this.onRoomError,
+ this.onRoomExit,
+ this.enterRoomTimeout = const Duration(seconds: 8),
+ }) : _cloudFactory =
+ cloudFactory ?? (() => TencentRoomTrtcCloud.sharedInstance());
+
+ static const int _roomMusicId = 1;
+
+ final Future Function() _cloudFactory;
+ final RoomRtcVoiceVolumeListener? onVoiceVolume;
+ final RoomMusicMixingStateListener? onAudioMixingStateChanged;
+ final RoomRtcRoomErrorListener? onRoomError;
+ final RoomRtcRoomExitListener? onRoomExit;
+ final Duration enterRoomTimeout;
+
+ RoomTrtcCloud? _cloud;
+ RoomTrtcAudioEffectManager? _audioEffectManager;
+ TRTCCloudListener? _listener;
+ Completer? _enterRoomCompleter;
+ Completer? _exitRoomCompleter;
+ String? _localUserId;
+ String? _currentMusicPath;
+ bool _isEntered = false;
+ bool _isLocalAudioStarted = false;
+ bool _expectingLocalExit = false;
+
+ @override
+ RoomRtcEngineKind get kind => RoomRtcEngineKind.trtc;
+
+ @override
+ Future prewarm() async {}
+
+ Future _ensureCloud() async {
+ final existing = _cloud;
+ if (existing != null) {
+ return existing;
+ }
+ final cloud = await _cloudFactory();
+ final listener = _createListener();
+ cloud.registerListener(listener);
+ _listener = listener;
+ _cloud = cloud;
+ return cloud;
+ }
+
+ TRTCCloudListener _createListener() {
+ return TRTCCloudListener(
+ onEnterRoom: (result) {
+ final completer = _enterRoomCompleter;
+ if (result >= 0) {
+ _isEntered = true;
+ if (completer != null && !completer.isCompleted) {
+ completer.complete();
+ }
+ return;
+ }
+ final error = StateError('TRTC enter room failed: $result');
+ if (completer != null && !completer.isCompleted) {
+ completer.completeError(error);
+ } else {
+ onRoomError?.call(error);
+ }
+ },
+ onError: (errCode, errMsg) {
+ final error = StateError('TRTC error: $errCode $errMsg');
+ final completer = _enterRoomCompleter;
+ if (completer != null && !completer.isCompleted) {
+ completer.completeError(error);
+ return;
+ }
+ if (_isEntered) {
+ onRoomError?.call(error);
+ }
+ },
+ onExitRoom: (reason) {
+ final wasEntered = _isEntered;
+ _isEntered = false;
+ _isLocalAudioStarted = false;
+ final completer = _exitRoomCompleter;
+ if (completer != null && !completer.isCompleted) {
+ completer.complete();
+ }
+ final exitReason = _mapExitReason(reason);
+ if (wasEntered &&
+ !_expectingLocalExit &&
+ exitReason != RoomRtcExitReason.local) {
+ onRoomExit?.call(exitReason);
+ }
+ _expectingLocalExit = false;
+ },
+ onUserVoiceVolume: (userVolumes, totalVolume) {
+ final localUserId = (_localUserId ?? '').trim();
+ final mapped = userVolumes
+ .map((item) {
+ final normalizedUserId =
+ item.userId.trim().isEmpty ? localUserId : item.userId.trim();
+ return RoomRtcUserVolume(
+ userId: normalizedUserId,
+ volume: item.volume,
+ isLocalUser:
+ localUserId.isNotEmpty && normalizedUserId == localUserId,
+ );
+ })
+ .where((item) => item.userId.isNotEmpty)
+ .toList(growable: false);
+ onVoiceVolume?.call(mapped, totalVolume);
+ },
+ );
+ }
+
+ RoomRtcExitReason _mapExitReason(int reason) {
+ switch (reason) {
+ case 0:
+ return RoomRtcExitReason.local;
+ case 1:
+ return RoomRtcExitReason.kicked;
+ case 2:
+ return RoomRtcExitReason.dismissed;
+ default:
+ return RoomRtcExitReason.unknown;
+ }
+ }
+
+ @override
+ Future enterRoomAsAudience(RoomRtcEnterRoomConfig config) async {
+ if (!config.hasTrtcCredential) {
+ final error = StateError('TRTC room credential is invalid');
+ if (config.throwOnError) {
+ throw error;
+ }
+ onRoomError?.call(error);
+ return;
+ }
+
+ final cloud = await _ensureCloud();
+ _localUserId = config.identity.trtcUserId;
+ _isEntered = false;
+ _isLocalAudioStarted = false;
+ _expectingLocalExit = false;
+ final joinCompleter = Completer();
+ _enterRoomCompleter = joinCompleter;
+
+ try {
+ cloud.setDefaultStreamRecvMode(true, false);
+ cloud.enableAudioVolumeEvaluation(
+ true,
+ TRTCAudioVolumeEvaluateParams(interval: 500),
+ );
+ cloud.enterRoom(
+ TRTCParams(
+ sdkAppId: config.trtcSdkAppId,
+ userId: config.identity.trtcUserId,
+ userSig: config.trtcUserSig.trim(),
+ roomId: 0,
+ strRoomId: config.identity.trtcStrRoomId,
+ role: TRTCRoleType.audience,
+ ),
+ TRTCAppScene.voiceChatRoom,
+ );
+ await joinCompleter.future.timeout(enterRoomTimeout);
+ } catch (error) {
+ try {
+ cloud.exitRoom();
+ } catch (_) {}
+ if (config.throwOnError) {
+ rethrow;
+ }
+ onRoomError?.call(error);
+ } finally {
+ if (identical(_enterRoomCompleter, joinCompleter)) {
+ _enterRoomCompleter = null;
+ }
+ }
+ }
+
+ @override
+ Future switchToAnchor(RoomRtcAnchorConfig config) async {
+ final cloud = _cloud;
+ if (!_isEntered || cloud == null) {
+ return false;
+ }
+ try {
+ cloud.switchRole(TRTCRoleType.anchor);
+ cloud.startLocalAudio(TRTCAudioQuality.defaultMode);
+ cloud.muteLocalAudio(config.muted);
+ _isLocalAudioStarted = true;
+ return true;
+ } catch (_) {
+ return false;
+ }
+ }
+
+ @override
+ Future switchToAudience() async {
+ final cloud = _cloud;
+ if (cloud == null) {
+ return false;
+ }
+ try {
+ cloud.switchRole(TRTCRoleType.audience);
+ cloud.muteLocalAudio(true);
+ if (_isLocalAudioStarted) {
+ cloud.stopLocalAudio();
+ }
+ _isLocalAudioStarted = false;
+ return true;
+ } catch (_) {
+ return false;
+ }
+ }
+
+ @override
+ void muteAllRemoteAudio(bool muted) {
+ _cloud?.muteAllRemoteAudio(muted);
+ }
+
+ RoomTrtcAudioEffectManager? get _musicManager {
+ final existing = _audioEffectManager;
+ if (existing != null) {
+ return existing;
+ }
+ final cloud = _cloud;
+ if (cloud == null) {
+ return null;
+ }
+ return _audioEffectManager = cloud.getAudioEffectManager();
+ }
+
+ @override
+ Future startAudioMixing(RoomRtcAudioMixingConfig config) async {
+ final path = config.filePath.trim();
+ if (path.isEmpty) {
+ return;
+ }
+ final manager = _musicManager;
+ if (manager == null) {
+ throw StateError('TRTC audio effect manager is not ready');
+ }
+ _currentMusicPath = path;
+ manager.setMusicObserver(_roomMusicId, _createMusicObserver());
+ manager.startPlayMusic(
+ AudioMusicParam(
+ id: _roomMusicId,
+ path: path,
+ loopCount: _audioMixingCycleToTrtcLoopCount(config.cycle),
+ publish: !config.loopback,
+ startTimeMS: config.startPos,
+ ),
+ );
+ }
+
+ int _audioMixingCycleToTrtcLoopCount(int cycle) {
+ if (cycle < 0) {
+ return 1000000;
+ }
+ if (cycle <= 1) {
+ return 0;
+ }
+ return cycle - 1;
+ }
+
+ TXMusicPlayObserver _createMusicObserver() {
+ return TXMusicPlayObserver(
+ onStart: (id, errorCode) {
+ if (id != _roomMusicId) {
+ return;
+ }
+ if (errorCode == 0) {
+ onAudioMixingStateChanged?.call(
+ RoomRtcAudioMixingState.playing,
+ RoomRtcAudioMixingReason.ok,
+ );
+ } else {
+ onAudioMixingStateChanged?.call(
+ RoomRtcAudioMixingState.failed,
+ RoomRtcAudioMixingReason.failed,
+ );
+ }
+ },
+ onPlayProgress: (_, __, ___) {},
+ onComplete: (id, errorCode) {
+ if (id != _roomMusicId) {
+ return;
+ }
+ onAudioMixingStateChanged?.call(
+ errorCode == 0
+ ? RoomRtcAudioMixingState.stopped
+ : RoomRtcAudioMixingState.failed,
+ errorCode == 0
+ ? RoomRtcAudioMixingReason.allLoopsCompleted
+ : RoomRtcAudioMixingReason.failed,
+ );
+ },
+ );
+ }
+
+ @override
+ Future pauseAudioMixing() async {
+ _musicManager?.pausePlayMusic(_roomMusicId);
+ onAudioMixingStateChanged?.call(
+ RoomRtcAudioMixingState.paused,
+ RoomRtcAudioMixingReason.ok,
+ );
+ }
+
+ @override
+ Future resumeAudioMixing() async {
+ _musicManager?.resumePlayMusic(_roomMusicId);
+ onAudioMixingStateChanged?.call(
+ RoomRtcAudioMixingState.playing,
+ RoomRtcAudioMixingReason.ok,
+ );
+ }
+
+ @override
+ Future stopAudioMixing() async {
+ _musicManager?.stopPlayMusic(_roomMusicId);
+ onAudioMixingStateChanged?.call(
+ RoomRtcAudioMixingState.stopped,
+ RoomRtcAudioMixingReason.stoppedByUser,
+ );
+ }
+
+ @override
+ Future setAudioMixingPosition(int positionMs) async {
+ _musicManager?.seekMusicToPosInTime(_roomMusicId, positionMs);
+ }
+
+ @override
+ Future getAudioMixingPosition() async {
+ return _musicManager?.getMusicCurrentPosInMS(_roomMusicId) ?? 0;
+ }
+
+ @override
+ Future getAudioMixingDuration() async {
+ final path = (_currentMusicPath ?? '').trim();
+ if (path.isEmpty) {
+ return 0;
+ }
+ return _musicManager?.getMusicDurationInMS(path) ?? 0;
+ }
+
+ @override
+ Future setAudioMixingVolume(int volume) async {
+ final safeVolume = volume.clamp(0, 100).toInt();
+ _musicManager?.setMusicPlayoutVolume(_roomMusicId, safeVolume);
+ _musicManager?.setMusicPublishVolume(_roomMusicId, safeVolume);
+ }
+
+ @override
+ Future leaveRoom() async {
+ final cloud = _cloud;
+ if (cloud == null) {
+ _isEntered = false;
+ _isLocalAudioStarted = false;
+ return;
+ }
+ try {
+ await stopAudioMixing();
+ } catch (_) {}
+ try {
+ if (_isLocalAudioStarted) {
+ cloud.stopLocalAudio();
+ }
+ cloud.enableAudioVolumeEvaluation(
+ false,
+ TRTCAudioVolumeEvaluateParams(interval: 0),
+ );
+ } catch (_) {}
+ if (!_isEntered) {
+ _isLocalAudioStarted = false;
+ return;
+ }
+ final exitCompleter = Completer();
+ _exitRoomCompleter = exitCompleter;
+ _expectingLocalExit = true;
+ try {
+ cloud.exitRoom();
+ await exitCompleter.future.timeout(const Duration(seconds: 3));
+ } catch (_) {
+ _isEntered = false;
+ _isLocalAudioStarted = false;
+ _expectingLocalExit = false;
+ } finally {
+ if (identical(_exitRoomCompleter, exitCompleter)) {
+ _exitRoomCompleter = null;
+ }
+ }
+ }
+
+ @override
+ Future release() async {
+ final cloud = _cloud;
+ final listener = _listener;
+ await leaveRoom();
+ try {
+ if (cloud != null && listener != null) {
+ cloud.unRegisterListener(listener);
+ }
+ TencentRoomTrtcCloud.destroySharedInstance();
+ } catch (_) {}
+ _listener = null;
+ _cloud = null;
+ _audioEffectManager = null;
+ _currentMusicPath = null;
+ _localUserId = null;
+ _isEntered = false;
+ _isLocalAudioStarted = false;
+ _expectingLocalExit = false;
+ }
+}
diff --git a/lib/services/localization/localization_manager.dart b/lib/services/localization/localization_manager.dart
index 0c4b173..9be5c51 100644
--- a/lib/services/localization/localization_manager.dart
+++ b/lib/services/localization/localization_manager.dart
@@ -4,6 +4,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
+import 'package:yumi/app_localizations.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
class LocalizationManager with ChangeNotifier {
@@ -19,31 +20,19 @@ class LocalizationManager with ChangeNotifier {
}
Locale _resolveInitialLocale() {
- String language = DataPersistence.getLang();
+ final language = DataPersistence.getLang();
if (language.isEmpty) {
- Locale systemLocale = ui.PlatformDispatcher.instance.locale;
- if (systemLocale.languageCode.startsWith("ar")) {
- return const Locale('ar', '');
- } else if (systemLocale.languageCode == "en") {
- return const Locale('en', '');
- } else if (systemLocale.languageCode == "tr") {
- return const Locale('tr', '');
- } else if (systemLocale.languageCode == "bn") {
- return const Locale('bn', '');
- } else {
- return const Locale('en', '');
- }
- } else {
- if (language.startsWith("ar")) {
- return const Locale('ar', '');
- } else if (language == "tr") {
- return const Locale('tr', '');
- } else if (language == "bn") {
- return const Locale('bn', '');
- } else {
- return const Locale('en', '');
- }
+ final systemLocale = ui.PlatformDispatcher.instance.locale;
+ return _localeFromLanguageCode(systemLocale.languageCode);
}
+ return _localeFromLanguageCode(language);
+ }
+
+ Locale _localeFromLanguageCode(String languageCode) {
+ return Locale(
+ SCAppLocalizations.resolveSupportedLanguageCode(languageCode),
+ '',
+ );
}
void changeAppLanguage(Locale locale) {
@@ -51,9 +40,9 @@ class LocalizationManager with ChangeNotifier {
_locale?.countryCode == locale.countryCode) {
return;
}
- _locale = locale;
- SCGlobalConfig.lang = locale.languageCode;
- unawaited(DataPersistence.setLang(locale.languageCode));
+ _locale = _localeFromLanguageCode(locale.languageCode);
+ SCGlobalConfig.lang = _locale!.languageCode;
+ unawaited(DataPersistence.setLang(_locale!.languageCode));
notifyListeners();
}
}
diff --git a/lib/services/music/room_music_manager.dart b/lib/services/music/room_music_manager.dart
index efaaeeb..4289c12 100644
--- a/lib/services/music/room_music_manager.dart
+++ b/lib/services/music/room_music_manager.dart
@@ -2,9 +2,9 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
-import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/foundation.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
+import 'package:yumi/services/audio/room_rtc_types.dart';
import 'package:yumi/services/music/room_music_repository.dart';
import 'package:yumi/shared/data_sources/models/sc_music_folder_mode.dart';
import 'package:yumi/shared/data_sources/models/sc_music_mode.dart';
@@ -436,25 +436,25 @@ class RoomMusicManager extends ChangeNotifier {
}
void _handleMixingStateChanged(
- AudioMixingStateType state,
- AudioMixingReasonType reason,
+ RoomRtcAudioMixingState state,
+ RoomRtcAudioMixingReason reason,
) {
switch (state) {
- case AudioMixingStateType.audioMixingStatePlaying:
+ case RoomRtcAudioMixingState.playing:
_isPlaying = true;
_isPaused = false;
_startPositionTimer();
_syncPublishingState();
notifyListeners();
break;
- case AudioMixingStateType.audioMixingStatePaused:
+ case RoomRtcAudioMixingState.paused:
_isPlaying = false;
_isPaused = true;
_stopPositionTimer();
_syncPublishingState();
notifyListeners();
break;
- case AudioMixingStateType.audioMixingStateStopped:
+ case RoomRtcAudioMixingState.stopped:
if (_isRoutingSwitching) {
return;
}
@@ -462,8 +462,7 @@ class RoomMusicManager extends ChangeNotifier {
return;
}
_stopPositionTimer();
- if (reason ==
- AudioMixingReasonType.audioMixingReasonAllLoopsCompleted) {
+ if (reason == RoomRtcAudioMixingReason.allLoopsCompleted) {
unawaited(next(fromAutoComplete: true));
return;
}
@@ -475,7 +474,7 @@ class RoomMusicManager extends ChangeNotifier {
notifyListeners();
}
break;
- case AudioMixingStateType.audioMixingStateFailed:
+ case RoomRtcAudioMixingState.failed:
_stopPositionTimer();
_isPlaying = false;
_isPaused = false;
diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart
index 9109c50..ade1790 100644
--- a/lib/shared/business_logic/models/res/login_res.dart
+++ b/lib/shared/business_logic/models/res/login_res.dart
@@ -180,14 +180,14 @@ class SocialChatUserProfile {
_backgroundPhotos = backgroundPhotos;
_personalPhotos = personalPhotos;
_cpList = cpList;
- }
-
- SocialChatUserProfile.fromJson(dynamic json) {
- final sourceJson = json;
- if (sourceJson is Map && sourceJson['userProfile'] is Map) {
- json = sourceJson['userProfile'];
- }
- _account = json['account'];
+ }
+
+ SocialChatUserProfile.fromJson(dynamic json) {
+ final sourceJson = json;
+ if (sourceJson is Map && sourceJson['userProfile'] is Map) {
+ json = sourceJson['userProfile'];
+ }
+ _account = json['account'];
_accountStatus = json['accountStatus'];
_age = json['age'];
_bornDay = json['bornDay'];
@@ -210,10 +210,10 @@ class SocialChatUserProfile {
json['ownSpecialId'] != null
? OwnSpecialId.fromJson(json['ownSpecialId'])
: null;
- _vipLevel = _firstNonBlankString([
- ..._vipLevelHintValues(json),
- ..._vipLevelHintValues(sourceJson),
- ]);
+ _vipLevel = _firstNonBlankString([
+ ..._vipLevelHintValues(json),
+ ..._vipLevelHintValues(sourceJson),
+ ]);
_sameRegion = json['sameRegion'];
_isCpRelation = json['isCpRelation'];
_firstRechargeEndTime = json['firstRechargeEndTime'];
@@ -654,100 +654,100 @@ class SocialChatUserProfile {
return resourceLevel!;
}
- return vipResource == null ? 0 : 1;
- }
-
- int get vipVisualResourceLevelForColoredId {
- for (final resource in [
- getVipBadge(),
- getHeaddress(),
- getChatBox(),
- getFloatPicture(),
- getDataCard(),
- getMountains(),
- ]) {
- final level = _vipLevelFromVisualResource(resource);
- if ((level ?? 0) > 0) {
- return level!;
- }
- }
- return 0;
- }
-
- /// ID 数字渐变字的 VIP 条件。
- /// 新 VIP 接口优先读等级字段;旧数据继续兼容 useProps 里的 NOBLE_VIP。
- bool hasVipPrivilegeForColoredId() {
- return vipLevelForColoredId > 0;
- }
-
- bool hasVipVisualResourceForColoredId() {
- return vipVisualResourceLevelForColoredId > 0;
- }
-
- bool shouldShowColoredSpecialIdText() {
- return hasVipPrivilegeForColoredId() || hasVipVisualResourceForColoredId();
- }
+ return vipResource == null ? 0 : 1;
+ }
+
+ int get vipVisualResourceLevelForColoredId {
+ for (final resource in [
+ getVipBadge(),
+ getHeaddress(),
+ getChatBox(),
+ getFloatPicture(),
+ getDataCard(),
+ getMountains(),
+ ]) {
+ final level = _vipLevelFromVisualResource(resource);
+ if ((level ?? 0) > 0) {
+ return level!;
+ }
+ }
+ return 0;
+ }
+
+ /// ID 数字渐变字的 VIP 条件。
+ /// 新 VIP 接口优先读等级字段;旧数据继续兼容 useProps 里的 NOBLE_VIP。
+ bool hasVipPrivilegeForColoredId() {
+ return vipLevelForColoredId > 0;
+ }
+
+ bool hasVipVisualResourceForColoredId() {
+ return vipVisualResourceLevelForColoredId > 0;
+ }
+
+ bool shouldShowColoredSpecialIdText() {
+ return hasVipPrivilegeForColoredId() || hasVipVisualResourceForColoredId();
+ }
void setHeartbeatVal(num? heartbeatVal) {
_heartbeatVal = (_heartbeatVal ?? 0) + (heartbeatVal ?? 0);
}
-}
-
-String? socialChatVipLevelHintFromJson(dynamic json) {
- return _firstNonBlankString(_vipLevelHintValues(json));
-}
-
-SocialChatUserProfile? socialChatProfileWithVipLevelHint(
- SocialChatUserProfile? profile,
- dynamic json,
-) {
- final vipLevel = socialChatVipLevelHintFromJson(json);
- if (profile == null || vipLevel == null) {
- return profile;
- }
- return profile.copyWith(vipLevel: vipLevel);
-}
-
-SocialChatUserProfile? socialChatUserProfileFromJsonWithVipHint(
- dynamic json, {
- String userProfileKey = 'userProfile',
-}) {
- if (json == null) {
- return null;
- }
- if (json is Map && json[userProfileKey] != null) {
- return socialChatProfileWithVipLevelHint(
- SocialChatUserProfile.fromJson(json[userProfileKey]),
- json,
- );
- }
- return SocialChatUserProfile.fromJson(json);
-}
-
-Iterable _vipLevelHintValues(dynamic json) sync* {
- if (json is! Map) {
- return;
- }
- yield json['vipLevel'];
- yield json['svipLevel'];
- yield json['sVipLevel'];
- yield json['userSVipLevel'];
- yield json['supperVip'];
- yield json['superVip'];
- yield json['nobleVipLevel'];
- yield json['vipLevelCode'];
- yield json['levelCode'];
- yield _nestedVipLevel(json['vipStatus']);
- yield _nestedVipLevel(json['vipState']);
- yield _nestedVipLevel(json['vip']);
- final nestedProfile = json['userProfile'];
- if (nestedProfile is Map) {
- yield* _vipLevelHintValues(nestedProfile);
- }
-}
-
-String? _firstNonBlankString(Iterable values) {
- for (final value in values) {
+}
+
+String? socialChatVipLevelHintFromJson(dynamic json) {
+ return _firstNonBlankString(_vipLevelHintValues(json));
+}
+
+SocialChatUserProfile? socialChatProfileWithVipLevelHint(
+ SocialChatUserProfile? profile,
+ dynamic json,
+) {
+ final vipLevel = socialChatVipLevelHintFromJson(json);
+ if (profile == null || vipLevel == null) {
+ return profile;
+ }
+ return profile.copyWith(vipLevel: vipLevel);
+}
+
+SocialChatUserProfile? socialChatUserProfileFromJsonWithVipHint(
+ dynamic json, {
+ String userProfileKey = 'userProfile',
+}) {
+ if (json == null) {
+ return null;
+ }
+ if (json is Map && json[userProfileKey] != null) {
+ return socialChatProfileWithVipLevelHint(
+ SocialChatUserProfile.fromJson(json[userProfileKey]),
+ json,
+ );
+ }
+ return SocialChatUserProfile.fromJson(json);
+}
+
+Iterable _vipLevelHintValues(dynamic json) sync* {
+ if (json is! Map) {
+ return;
+ }
+ yield json['vipLevel'];
+ yield json['svipLevel'];
+ yield json['sVipLevel'];
+ yield json['userSVipLevel'];
+ yield json['supperVip'];
+ yield json['superVip'];
+ yield json['nobleVipLevel'];
+ yield json['vipLevelCode'];
+ yield json['levelCode'];
+ yield _nestedVipLevel(json['vipStatus']);
+ yield _nestedVipLevel(json['vipState']);
+ yield _nestedVipLevel(json['vip']);
+ final nestedProfile = json['userProfile'];
+ if (nestedProfile is Map) {
+ yield* _vipLevelHintValues(nestedProfile);
+ }
+}
+
+String? _firstNonBlankString(Iterable values) {
+ for (final value in values) {
final text = value?.toString().trim();
if (text != null && text.isNotEmpty) {
return text;
@@ -756,54 +756,41 @@ String? _firstNonBlankString(Iterable values) {
return null;
}
-dynamic _nestedVipLevel(dynamic value) {
- if (value is Map) {
- final active = value['active'];
- if (active == false ||
- active?.toString().trim().toLowerCase() == 'false') {
- return '0';
- }
- return value['level'] ??
- value['vipLevel'] ??
+dynamic _nestedVipLevel(dynamic value) {
+ if (value is Map) {
+ final active = value['active'];
+ if (active == false || active?.toString().trim().toLowerCase() == 'false') {
+ return '0';
+ }
+ return value['level'] ??
+ value['vipLevel'] ??
value['userSVipLevel'] ??
value['supperVip'] ??
value['levelCode'];
}
- return null;
-}
-
-int? _vipLevelFromVisualResource(PropsResources? resource) {
- if (resource == null) {
- return null;
- }
- return _parseVipLevelForColoredId(
- resource.name,
- allowPlainNumber: false,
- ) ??
- _parseVipLevelForColoredId(
- resource.code,
- allowPlainNumber: false,
- ) ??
- _parseVipLevelForColoredId(
- resource.cover,
- allowPlainNumber: false,
- ) ??
- _parseVipLevelForColoredId(
- resource.sourceUrl,
- allowPlainNumber: false,
- ) ??
- _parseVipLevelForColoredId(
- resource.roomOpenedUrl,
- allowPlainNumber: false,
- ) ??
- _parseVipLevelForColoredId(
- resource.roomSendCoverUrl,
- allowPlainNumber: false,
- );
-}
-
-int? _parseVipLevelForColoredId(
- dynamic value, {
+ return null;
+}
+
+int? _vipLevelFromVisualResource(PropsResources? resource) {
+ if (resource == null) {
+ return null;
+ }
+ return _parseVipLevelForColoredId(resource.name, allowPlainNumber: false) ??
+ _parseVipLevelForColoredId(resource.code, allowPlainNumber: false) ??
+ _parseVipLevelForColoredId(resource.cover, allowPlainNumber: false) ??
+ _parseVipLevelForColoredId(resource.sourceUrl, allowPlainNumber: false) ??
+ _parseVipLevelForColoredId(
+ resource.roomOpenedUrl,
+ allowPlainNumber: false,
+ ) ??
+ _parseVipLevelForColoredId(
+ resource.roomSendCoverUrl,
+ allowPlainNumber: false,
+ );
+}
+
+int? _parseVipLevelForColoredId(
+ dynamic value, {
bool allowPlainNumber = true,
bool allowTrailingNumber = true,
}) {
@@ -878,30 +865,25 @@ class WearBadge {
_userId = 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']);
- }
+ 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']);
+ _type = _stringValue(json['type']);
+ _userId = _stringValue(json['userId']);
+ }
String? _animationUrl;
String? _badgeKey;
@@ -968,7 +950,7 @@ class WearBadge {
String? get userId => _userId;
- Map toJson() {
+ Map toJson() {
final map = {};
map['animationUrl'] = _animationUrl;
map['badgeKey'] = _badgeKey;
@@ -982,48 +964,48 @@ class WearBadge {
map['selectUrl'] = _selectUrl;
map['type'] = _type;
map['userId'] = _userId;
- 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;
- }
-}
+ 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}) {
diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart
index 5c70817..32f0ffb 100644
--- a/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart
+++ b/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart
@@ -10,6 +10,8 @@ class SCRoomRocketLevelRes {
required this.shakeThresholdPercent,
required this.enabled,
required this.sort,
+ this.rewardPreview,
+ this.rocketKings = const [],
});
factory SCRoomRocketLevelRes.fromJson(dynamic json) {
@@ -192,6 +194,40 @@ class SCRoomRocketLevelRes {
95,
enabled: _boolValue(data['enabled'], fallback: true),
sort: _intValue(data['sort']),
+ rewardPreview: SCRoomRocketRewardPreviewRes.tryFromJson(
+ _firstPayload([
+ data['rewardPreview'],
+ data['reward_preview'],
+ data['rewardConfig'],
+ data['reward_config'],
+ data['rewards'],
+ data['rewardList'],
+ levelInfo['rewardPreview'],
+ levelInfo['rewardConfig'],
+ levelInfo['rewards'],
+ levelInfo['rewardList'],
+ config['rewardPreview'],
+ config['rewardConfig'],
+ config['rewards'],
+ config['rewardList'],
+ data,
+ ]),
+ ),
+ rocketKings: SCRoomRocketKingUserRes.listFromJson(
+ _firstPayload([
+ data['rocketKings'],
+ data['rocket_kings'],
+ data['kings'],
+ data['kingUsers'],
+ data['podium'],
+ levelInfo['rocketKings'],
+ levelInfo['kings'],
+ levelInfo['podium'],
+ config['rocketKings'],
+ config['kings'],
+ config['podium'],
+ ]),
+ ),
);
}
@@ -205,6 +241,8 @@ class SCRoomRocketLevelRes {
final num shakeThresholdPercent;
final bool enabled;
final int sort;
+ final SCRoomRocketRewardPreviewRes? rewardPreview;
+ final List rocketKings;
Map toJson() {
return {
@@ -218,6 +256,18 @@ class SCRoomRocketLevelRes {
'shakeThresholdPercent': shakeThresholdPercent,
'enabled': enabled,
'sort': sort,
+ 'rewardPreview':
+ rewardPreview == null
+ ? null
+ : {
+ 'top1':
+ rewardPreview!.top1.map((item) => item.toJson()).toList(),
+ 'ignite':
+ rewardPreview!.ignite.map((item) => item.toJson()).toList(),
+ 'inRoom':
+ rewardPreview!.inRoom.map((item) => item.toJson()).toList(),
+ },
+ 'rocketKings': rocketKings.map((item) => item.toJson()).toList(),
};
}
}
@@ -229,6 +279,7 @@ class SCRoomRocketRewardRes {
required this.rewardItemId,
required this.rewardName,
required this.rewardCover,
+ required this.rocketIconUrl,
required this.rewardAmount,
required this.expireDays,
});
@@ -241,6 +292,21 @@ class SCRoomRocketRewardRes {
rewardItemId: _stringValue(data['rewardItemId']),
rewardName: _stringValue(data['rewardName']),
rewardCover: _stringValue(data['rewardCover']),
+ rocketIconUrl: _firstAssetString([
+ data['rocket_icon_url'],
+ data['rocketIconUrl'],
+ data['rocketIcon'],
+ data['iconUrl'],
+ _asMap(data['rocket'])['rocket_icon_url'],
+ _asMap(data['rocket'])['rocketIconUrl'],
+ _asMap(data['rocket'])['rocketIcon'],
+ _asMap(data['levelInfo'])['rocket_icon_url'],
+ _asMap(data['levelInfo'])['rocketIconUrl'],
+ _asMap(data['levelInfo'])['rocketIcon'],
+ _asMap(data['config'])['rocket_icon_url'],
+ _asMap(data['config'])['rocketIconUrl'],
+ _asMap(data['config'])['rocketIcon'],
+ ]),
rewardAmount: _numValue(data['rewardAmount']) ?? 0,
expireDays: _intValue(data['expireDays']),
);
@@ -251,9 +317,23 @@ class SCRoomRocketRewardRes {
final String rewardItemId;
final String rewardName;
final String rewardCover;
+ final String rocketIconUrl;
final num rewardAmount;
final int expireDays;
+ Map toJson() {
+ return {
+ 'rewardScene': rewardScene,
+ 'rewardType': rewardType,
+ 'rewardItemId': rewardItemId,
+ 'rewardName': rewardName,
+ 'rewardCover': rewardCover,
+ 'rocketIconUrl': rocketIconUrl,
+ 'rewardAmount': rewardAmount,
+ 'expireDays': expireDays,
+ };
+ }
+
String get displayAmount {
if (rewardAmount <= 0) {
return '1';
@@ -273,11 +353,80 @@ class SCRoomRocketRewardPreviewRes {
});
factory SCRoomRocketRewardPreviewRes.fromJson(dynamic json) {
+ if (json is Iterable && json is! String) {
+ return _fromFlatRewards(_rewardList(json));
+ }
final data = _asMap(json);
- return SCRoomRocketRewardPreviewRes(
- top1: _rewardList(data['top1']),
- ignite: _rewardList(data['ignite']),
- inRoom: _rewardList(data['inRoom']),
+ final nested = tryFromJson(
+ _firstPayload([
+ data['rewardPreview'],
+ data['reward_preview'],
+ data['rewardConfig'],
+ data['reward_config'],
+ ]),
+ );
+ if (nested != null) {
+ return nested;
+ }
+ final preview = SCRoomRocketRewardPreviewRes(
+ top1: _rewardList(
+ _firstPayload([
+ data['top1'],
+ data['top_1'],
+ data['topOne'],
+ data['topOneRewards'],
+ data['top1Rewards'],
+ data['rank1'],
+ data['rankOne'],
+ data['first'],
+ data['firstRewards'],
+ data['king'],
+ data['rocketKing'],
+ ]),
+ ),
+ ignite: _rewardList(
+ _firstPayload([
+ data['ignite'],
+ data['igniteRewards'],
+ data['setOff'],
+ data['set_off'],
+ data['setOffRewards'],
+ data['launch'],
+ data['launchRewards'],
+ data['launcher'],
+ data['trigger'],
+ data['triggerRewards'],
+ ]),
+ ),
+ inRoom: _rewardList(
+ _firstPayload([
+ data['inRoom'],
+ data['in_room'],
+ data['inRoomRewards'],
+ data['room'],
+ data['roomRewards'],
+ data['audience'],
+ data['audienceRewards'],
+ data['participants'],
+ data['allUsers'],
+ data['allUserRewards'],
+ ]),
+ ),
+ );
+ if (preview.isNotEmpty) {
+ return preview;
+ }
+ return _fromFlatRewards(
+ _rewardList(
+ _firstPayload([
+ data['rewards'],
+ data['rewardList'],
+ data['rewardPreviewList'],
+ data['items'],
+ data['list'],
+ data['records'],
+ ]),
+ ),
);
}
@@ -287,12 +436,86 @@ class SCRoomRocketRewardPreviewRes {
List get all => [...top1, ...ignite, ...inRoom];
+ bool get isEmpty => top1.isEmpty && ignite.isEmpty && inRoom.isEmpty;
+
+ bool get isNotEmpty => !isEmpty;
+
+ static SCRoomRocketRewardPreviewRes? tryFromJson(dynamic json) {
+ if (!_hasPayload(json)) {
+ return null;
+ }
+ final preview = SCRoomRocketRewardPreviewRes.fromJson(json);
+ return preview.isEmpty ? null : preview;
+ }
+
static List _rewardList(dynamic value) {
- if (value is! Iterable) {
+ if (!_hasPayload(value)) {
+ return const [];
+ }
+ if (value is Map) {
+ final data = _asMap(value);
+ if (_looksLikeReward(data)) {
+ return [SCRoomRocketRewardRes.fromJson(data)];
+ }
+ return data.values.expand(_rewardList).toList(growable: false);
+ }
+ if (value is! Iterable || value is String) {
return const [];
}
return value.map(SCRoomRocketRewardRes.fromJson).toList(growable: false);
}
+
+ static SCRoomRocketRewardPreviewRes _fromFlatRewards(
+ List rewards,
+ ) {
+ if (rewards.isEmpty) {
+ return SCRoomRocketRewardPreviewRes(
+ top1: const [],
+ ignite: const [],
+ inRoom: const [],
+ );
+ }
+ final top1 = [];
+ final ignite = [];
+ final inRoom = [];
+ for (final reward in rewards) {
+ final scene = _normalizedScene(reward.rewardScene);
+ if (scene.contains('TOP1') ||
+ scene.contains('TOP_1') ||
+ scene.contains('RANK1') ||
+ scene.contains('RANK_1') ||
+ scene.contains('FIRST') ||
+ scene.contains('KING')) {
+ top1.add(reward);
+ } else if (scene.contains('IGNITE') ||
+ scene.contains('SET_OFF') ||
+ scene.contains('SETOFF') ||
+ scene.contains('LAUNCH') ||
+ scene.contains('TRIGGER')) {
+ ignite.add(reward);
+ } else {
+ inRoom.add(reward);
+ }
+ }
+ return SCRoomRocketRewardPreviewRes(
+ top1: top1,
+ ignite: ignite,
+ inRoom: inRoom,
+ );
+ }
+
+ static bool _looksLikeReward(Map data) {
+ return data.containsKey('rewardScene') ||
+ data.containsKey('rewardType') ||
+ data.containsKey('rewardItemId') ||
+ data.containsKey('rewardName') ||
+ data.containsKey('rewardCover') ||
+ data.containsKey('rewardAmount');
+ }
+
+ static String _normalizedScene(String value) {
+ return value.trim().toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]+'), '_');
+ }
}
class SCRoomRocketKingUserRes {
@@ -344,6 +567,19 @@ class SCRoomRocketKingUserRes {
final String countryName;
final num scoreEnergy;
+ Map toJson() {
+ return {
+ 'rank': rank,
+ 'userId': userId,
+ 'userAvatar': userAvatar,
+ 'userNickname': userNickname,
+ 'account': account,
+ 'countryCode': countryCode,
+ 'countryName': countryName,
+ 'scoreEnergy': scoreEnergy,
+ };
+ }
+
String get displayName {
if (userNickname.trim().isNotEmpty) {
return userNickname;
@@ -353,6 +589,44 @@ class SCRoomRocketKingUserRes {
}
return userId;
}
+
+ static List listFromJson(dynamic value) {
+ if (!_hasPayload(value)) {
+ return const [];
+ }
+ if (value is Map) {
+ final data = _asMap(value);
+ final nested = _firstPayload([
+ data['records'],
+ data['podium'],
+ data['rocketKings'],
+ data['kings'],
+ data['items'],
+ data['list'],
+ ]);
+ if (nested != null) {
+ return listFromJson(nested);
+ }
+ if (_looksLikeKingUser(data)) {
+ return [SCRoomRocketKingUserRes.fromJson(data)];
+ }
+ return data.values.expand(listFromJson).toList(growable: false);
+ }
+ if (value is! Iterable || value is String) {
+ return const [];
+ }
+ return value.map(SCRoomRocketKingUserRes.fromJson).toList(growable: false);
+ }
+
+ static bool _looksLikeKingUser(Map data) {
+ return data.containsKey('rank') ||
+ data.containsKey('ranking') ||
+ data.containsKey('userId') ||
+ data.containsKey('userAvatar') ||
+ data.containsKey('avatar') ||
+ data.containsKey('userProfile') ||
+ data.containsKey('user');
+ }
}
class SCRoomRocketKingRes {
@@ -403,10 +677,7 @@ class SCRoomRocketKingRes {
}
static List _kingList(dynamic value) {
- if (value is! Iterable) {
- return const [];
- }
- return value.map(SCRoomRocketKingUserRes.fromJson).toList(growable: false);
+ return SCRoomRocketKingUserRes.listFromJson(value);
}
static SCRoomRocketKingUserRes? _kingUser(dynamic value) {
@@ -501,6 +772,7 @@ class SCRoomRocketRewardRecordRes {
required this.rewardName,
required this.rewardCover,
required this.rewardAmount,
+ required this.rocketIconUrl,
required this.expireDays,
required this.grantStatus,
required this.popupStatus,
@@ -544,6 +816,21 @@ class SCRoomRocketRewardRecordRes {
rewardName: _stringValue(data['rewardName']),
rewardCover: _stringValue(data['rewardCover']),
rewardAmount: _numValue(data['rewardAmount']) ?? 0,
+ rocketIconUrl: _firstAssetString([
+ data['rocket_icon_url'],
+ data['rocketIconUrl'],
+ data['rocketIcon'],
+ data['iconUrl'],
+ _asMap(data['rocket'])['rocket_icon_url'],
+ _asMap(data['rocket'])['rocketIconUrl'],
+ _asMap(data['rocket'])['rocketIcon'],
+ _asMap(data['levelInfo'])['rocket_icon_url'],
+ _asMap(data['levelInfo'])['rocketIconUrl'],
+ _asMap(data['levelInfo'])['rocketIcon'],
+ _asMap(data['config'])['rocket_icon_url'],
+ _asMap(data['config'])['rocketIconUrl'],
+ _asMap(data['config'])['rocketIcon'],
+ ]),
expireDays: _intValue(data['expireDays']),
grantStatus: _stringValue(data['grantStatus']),
popupStatus: _stringValue(data['popupStatus']),
@@ -569,6 +856,7 @@ class SCRoomRocketRewardRecordRes {
final String rewardName;
final String rewardCover;
final num rewardAmount;
+ final String rocketIconUrl;
final int expireDays;
final String grantStatus;
final String popupStatus;
@@ -617,6 +905,7 @@ class SCRoomRocketRewardRecordRes {
"rewardName": rewardName,
"rewardCover": rewardCover,
"rewardAmount": rewardAmount,
+ "rocketIconUrl": rocketIconUrl,
"expireDays": expireDays,
"grantStatus": grantStatus,
"popupStatus": popupStatus,
@@ -709,6 +998,35 @@ String _assetString(dynamic value) {
return text;
}
+dynamic _firstPayload(Iterable values) {
+ for (final value in values) {
+ if (_hasPayload(value)) {
+ return value;
+ }
+ }
+ return null;
+}
+
+bool _hasPayload(dynamic value) {
+ if (value == null) {
+ return false;
+ }
+ if (value is String) {
+ final text = value.trim();
+ return text.isNotEmpty &&
+ text != '{}' &&
+ text != '[]' &&
+ text.toLowerCase() != 'null';
+ }
+ if (value is Map) {
+ return value.isNotEmpty;
+ }
+ if (value is Iterable) {
+ return value.isNotEmpty;
+ }
+ return true;
+}
+
int _intValue(dynamic value, {int fallback = 0}) {
if (value is int) {
return value;
diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_config_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_config_res.dart
index ed709c8..1cd00a1 100644
--- a/lib/shared/business_logic/models/res/sc_room_rocket_config_res.dart
+++ b/lib/shared/business_logic/models/res/sc_room_rocket_config_res.dart
@@ -1,186 +1,273 @@
-/// enabled : true
-/// id : 3
-/// level : 3
-/// levelName : "三级火箭"
-/// maxEnergy : 1000000
-/// normalRewardConfig : []
-/// rewardConfig : [{"amount":1969014438069481474,"content":"1969014438069481474","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-5058b89a-8654-4895-866e-fe4295e88408.png","createTime":1758286290000,"detailType":"AVATAR_FRAME","groupId":1966024426660499457,"id":1969021529953873921,"name":"yumi-KingTOP1头饰","quantity":7,"remark":"","sort":1,"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-975e24c5-e87a-4236-9256-799c127a06bc.svga","type":"PROPS","updateTime":1758286290000},{"amount":1969014872674873345,"badgeName":"yumi-King'Medal","content":"1969014872674873345","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/other/manager-fc5ad0a4-5e4a-498b-8494-2aee2efe5b99.png","createTime":1758286290000,"detailType":"BADGE","groupId":1966024426660499457,"id":1969021529953873922,"quantity":7,"remark":"","sort":2,"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/other/manager-176fcf7e-787b-4bf6-bdb4-238d8388a5e2.svga","type":"BADGE","updateTime":1758286290000},{"amount":30000,"content":"30000","createTime":1758286290000,"detailType":"GOLD","groupId":1966024426660499457,"id":1969021529953873923,"quantity":0,"remark":"","sort":3,"type":"GOLD","updateTime":1758286290000},{"amount":1962463978629165057,"content":"1962463978629165057","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-6633f4cb-58b8-4b7d-8075-7f0a6f29fae0.png","createTime":1758286290000,"detailType":"RIDE","groupId":1966024426660499457,"id":1969021529953873924,"name":"后续活动座驾","quantity":7,"remark":"","sort":4,"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-536ac482-8e9d-4a21-acff-531d51ff5b66.mp4","type":"PROPS","updateTime":1758286290000}]
-/// status : 1
-
-class SCRoomRocketConfigRes {
- SCRoomRocketConfigRes({
- bool? enabled,
- String? id,
- num? level,
- String? levelName,
- int? maxEnergy,
- List? rewardConfig,
- num? status,}){
- _enabled = enabled;
- _id = id;
- _level = level;
- _levelName = levelName;
- _maxEnergy = maxEnergy;
- _rewardConfig = rewardConfig;
- _status = status;
-}
-
- SCRoomRocketConfigRes.fromJson(dynamic json) {
- _enabled = json['enabled'];
- _id = json['id'];
- _level = json['level'];
- _levelName = json['levelName'];
- _maxEnergy = json['maxEnergy'];
- if (json['rewardConfig'] != null) {
- _rewardConfig = [];
- json['rewardConfig'].forEach((v) {
- _rewardConfig?.add(RewardConfig.fromJson(v));
- });
- }
- _status = json['status'];
- }
- bool? _enabled;
- String? _id;
- num? _level;
- String? _levelName;
- int? _maxEnergy;
- List? _rewardConfig;
- num? _status;
-
- bool? get enabled => _enabled;
- String? get id => _id;
- num? get level => _level;
- String? get levelName => _levelName;
- int? get maxEnergy => _maxEnergy;
- List? get rewardConfig => _rewardConfig;
- num? get status => _status;
-
- Map toJson() {
- final map = {};
- map['enabled'] = _enabled;
- map['id'] = _id;
- map['level'] = _level;
- map['levelName'] = _levelName;
- map['maxEnergy'] = _maxEnergy;
- if (_rewardConfig != null) {
- map['rewardConfig'] = _rewardConfig?.map((v) => v.toJson()).toList();
- }
- map['status'] = _status;
- return map;
- }
-
-}
-
-/// amount : 1969014438069481474
-/// content : "1969014438069481474"
-/// cover : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-5058b89a-8654-4895-866e-fe4295e88408.png"
-/// createTime : 1758286290000
-/// detailType : "AVATAR_FRAME"
-/// groupId : 1966024426660499457
-/// id : 1969021529953873921
-/// name : "Yumi-KingTOP1头饰"
-/// quantity : 7
-/// remark : ""
-/// sort : 1
-/// sourceUrl : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-975e24c5-e87a-4236-9256-799c127a06bc.svga"
-/// type : "PROPS"
-/// updateTime : 1758286290000
-
-class RewardConfig {
- RewardConfig({
- num? amount,
- String? content,
- String? cover,
- num? createTime,
- String? detailType,
- String? groupId,
- String? id,
- String? name,
- num? quantity,
- String? remark,
- num? sort,
- String? sourceUrl,
- String? type,
- num? updateTime,}){
- _amount = amount;
- _content = content;
- _cover = cover;
- _createTime = createTime;
- _detailType = detailType;
- _groupId = groupId;
- _id = id;
- _name = name;
- _quantity = quantity;
- _remark = remark;
- _sort = sort;
- _sourceUrl = sourceUrl;
- _type = type;
- _updateTime = updateTime;
-}
-
- RewardConfig.fromJson(dynamic json) {
- _amount = json['amount'];
- _content = json['content'];
- _cover = json['cover'];
- _createTime = json['createTime'];
- _detailType = json['detailType'];
- _groupId = json['groupId'];
- _id = json['id'];
- _name = json['name'];
- _quantity = json['quantity'];
- _remark = json['remark'];
- _sort = json['sort'];
- _sourceUrl = json['sourceUrl'];
- _type = json['type'];
- _updateTime = json['updateTime'];
- }
- num? _amount;
- String? _content;
- String? _cover;
- num? _createTime;
- String? _detailType;
- String? _groupId;
- String? _id;
- String? _name;
- num? _quantity;
- String? _remark;
- num? _sort;
- String? _sourceUrl;
- String? _type;
- num? _updateTime;
-
- num? get amount => _amount;
- String? get content => _content;
- String? get cover => _cover;
- num? get createTime => _createTime;
- String? get detailType => _detailType;
- String? get groupId => _groupId;
- String? get id => _id;
- String? get name => _name;
- num? get quantity => _quantity;
- String? get remark => _remark;
- num? get sort => _sort;
- String? get sourceUrl => _sourceUrl;
- String? get type => _type;
- num? get updateTime => _updateTime;
-
- Map toJson() {
- final map = {};
- map['amount'] = _amount;
- map['content'] = _content;
- map['cover'] = _cover;
- map['createTime'] = _createTime;
- map['detailType'] = _detailType;
- map['groupId'] = _groupId;
- map['id'] = _id;
- map['name'] = _name;
- map['quantity'] = _quantity;
- map['remark'] = _remark;
- map['sort'] = _sort;
- map['sourceUrl'] = _sourceUrl;
- map['type'] = _type;
- map['updateTime'] = _updateTime;
- return map;
- }
-
-}
\ No newline at end of file
+// enabled : true
+// id : 3
+// level : 3
+// levelName : "三级火箭"
+// maxEnergy : 1000000
+// normalRewardConfig : []
+// rewardConfig : [{"amount":1969014438069481474,"content":"1969014438069481474","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-5058b89a-8654-4895-866e-fe4295e88408.png","createTime":1758286290000,"detailType":"AVATAR_FRAME","groupId":1966024426660499457,"id":1969021529953873921,"name":"yumi-KingTOP1头饰","quantity":7,"remark":"","sort":1,"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-975e24c5-e87a-4236-9256-799c127a06bc.svga","type":"PROPS","updateTime":1758286290000},{"amount":1969014872674873345,"badgeName":"yumi-King'Medal","content":"1969014872674873345","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/other/manager-fc5ad0a4-5e4a-498b-8494-2aee2efe5b99.png","createTime":1758286290000,"detailType":"BADGE","groupId":1966024426660499457,"id":1969021529953873922,"quantity":7,"remark":"","sort":2,"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/other/manager-176fcf7e-787b-4bf6-bdb4-238d8388a5e2.svga","type":"BADGE","updateTime":1758286290000},{"amount":30000,"content":"30000","createTime":1758286290000,"detailType":"GOLD","groupId":1966024426660499457,"id":1969021529953873923,"quantity":0,"remark":"","sort":3,"type":"GOLD","updateTime":1758286290000},{"amount":1962463978629165057,"content":"1962463978629165057","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-6633f4cb-58b8-4b7d-8075-7f0a6f29fae0.png","createTime":1758286290000,"detailType":"RIDE","groupId":1966024426660499457,"id":1969021529953873924,"name":"后续活动座驾","quantity":7,"remark":"","sort":4,"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-536ac482-8e9d-4a21-acff-531d51ff5b66.mp4","type":"PROPS","updateTime":1758286290000}]
+// status : 1
+
+class SCRoomRocketConfigRes {
+ SCRoomRocketConfigRes({
+ bool? enabled,
+ String? id,
+ num? level,
+ String? levelName,
+ int? maxEnergy,
+ List? rewardConfig,
+ List? normalRewardConfig,
+ List? igniteRewardConfig,
+ List? inRoomRewardConfig,
+ num? status,
+ }) {
+ _enabled = enabled;
+ _id = id;
+ _level = level;
+ _levelName = levelName;
+ _maxEnergy = maxEnergy;
+ _rewardConfig = rewardConfig;
+ _normalRewardConfig = normalRewardConfig;
+ _igniteRewardConfig = igniteRewardConfig;
+ _inRoomRewardConfig = inRoomRewardConfig;
+ _status = status;
+ }
+
+ SCRoomRocketConfigRes.fromJson(dynamic json) {
+ _enabled = json['enabled'];
+ _id = json['id'];
+ _level = json['level'];
+ _levelName = json['levelName'];
+ _maxEnergy = json['maxEnergy'];
+ _rewardConfig = _rewardList(
+ json['rewardConfig'] ??
+ json['top1RewardConfig'] ??
+ json['topOneRewardConfig'] ??
+ json['kingRewardConfig'] ??
+ json['rocketKingRewardConfig'],
+ );
+ _normalRewardConfig = _rewardList(
+ json['normalRewardConfig'] ??
+ json['normalRewards'] ??
+ json['ordinaryRewardConfig'],
+ );
+ _igniteRewardConfig = _rewardList(
+ json['igniteRewardConfig'] ??
+ json['setOffRewardConfig'] ??
+ json['launchRewardConfig'] ??
+ json['triggerRewardConfig'],
+ );
+ _inRoomRewardConfig = _rewardList(
+ json['inRoomRewardConfig'] ??
+ json['roomRewardConfig'] ??
+ json['audienceRewardConfig'] ??
+ json['allUserRewardConfig'],
+ );
+ _status = json['status'];
+ }
+
+ bool? _enabled;
+ String? _id;
+ num? _level;
+ String? _levelName;
+ int? _maxEnergy;
+ List? _rewardConfig;
+ List? _normalRewardConfig;
+ List? _igniteRewardConfig;
+ List? _inRoomRewardConfig;
+ num? _status;
+
+ bool? get enabled => _enabled;
+
+ String? get id => _id;
+
+ num? get level => _level;
+
+ String? get levelName => _levelName;
+
+ int? get maxEnergy => _maxEnergy;
+
+ List? get rewardConfig => _rewardConfig;
+
+ List? get normalRewardConfig => _normalRewardConfig;
+
+ List? get igniteRewardConfig => _igniteRewardConfig;
+
+ List? get inRoomRewardConfig => _inRoomRewardConfig;
+
+ num? get status => _status;
+
+ Map toJson() {
+ final map = {};
+ map['enabled'] = _enabled;
+ map['id'] = _id;
+ map['level'] = _level;
+ map['levelName'] = _levelName;
+ map['maxEnergy'] = _maxEnergy;
+ if (_rewardConfig != null) {
+ map['rewardConfig'] = _rewardConfig?.map((v) => v.toJson()).toList();
+ }
+ if (_normalRewardConfig != null) {
+ map['normalRewardConfig'] =
+ _normalRewardConfig?.map((v) => v.toJson()).toList();
+ }
+ if (_igniteRewardConfig != null) {
+ map['igniteRewardConfig'] =
+ _igniteRewardConfig?.map((v) => v.toJson()).toList();
+ }
+ if (_inRoomRewardConfig != null) {
+ map['inRoomRewardConfig'] =
+ _inRoomRewardConfig?.map((v) => v.toJson()).toList();
+ }
+ map['status'] = _status;
+ return map;
+ }
+
+ static List? _rewardList(dynamic value) {
+ if (value is! Iterable) {
+ return null;
+ }
+ return value.map(RewardConfig.fromJson).toList(growable: false);
+ }
+}
+
+// amount : 1969014438069481474
+// content : "1969014438069481474"
+// cover : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-5058b89a-8654-4895-866e-fe4295e88408.png"
+// createTime : 1758286290000
+// detailType : "AVATAR_FRAME"
+// groupId : 1966024426660499457
+// id : 1969021529953873921
+// name : "Yumi-KingTOP1头饰"
+// quantity : 7
+// remark : ""
+// sort : 1
+// sourceUrl : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-975e24c5-e87a-4236-9256-799c127a06bc.svga"
+// type : "PROPS"
+// updateTime : 1758286290000
+
+class RewardConfig {
+ RewardConfig({
+ num? amount,
+ String? content,
+ String? cover,
+ num? createTime,
+ String? detailType,
+ String? groupId,
+ String? id,
+ String? name,
+ String? badgeName,
+ num? quantity,
+ String? remark,
+ num? sort,
+ String? sourceUrl,
+ String? type,
+ String? rewardScene,
+ num? updateTime,
+ }) {
+ _amount = amount;
+ _content = content;
+ _cover = cover;
+ _createTime = createTime;
+ _detailType = detailType;
+ _groupId = groupId;
+ _id = id;
+ _name = name;
+ _badgeName = badgeName;
+ _quantity = quantity;
+ _remark = remark;
+ _sort = sort;
+ _sourceUrl = sourceUrl;
+ _type = type;
+ _rewardScene = rewardScene;
+ _updateTime = updateTime;
+ }
+
+ RewardConfig.fromJson(dynamic json) {
+ _amount = json['amount'];
+ _content = json['content'];
+ _cover = json['cover'];
+ _createTime = json['createTime'];
+ _detailType = json['detailType'];
+ _groupId = json['groupId'];
+ _id = json['id'];
+ _name = json['name'];
+ _badgeName = json['badgeName'];
+ _quantity = json['quantity'];
+ _remark = json['remark'];
+ _sort = json['sort'];
+ _sourceUrl = json['sourceUrl'];
+ _type = json['type'];
+ _rewardScene = json['rewardScene'] ?? json['scene'];
+ _updateTime = json['updateTime'];
+ }
+
+ num? _amount;
+ String? _content;
+ String? _cover;
+ num? _createTime;
+ String? _detailType;
+ String? _groupId;
+ String? _id;
+ String? _name;
+ String? _badgeName;
+ num? _quantity;
+ String? _remark;
+ num? _sort;
+ String? _sourceUrl;
+ String? _type;
+ String? _rewardScene;
+ num? _updateTime;
+
+ num? get amount => _amount;
+
+ String? get content => _content;
+
+ String? get cover => _cover;
+
+ num? get createTime => _createTime;
+
+ String? get detailType => _detailType;
+
+ String? get groupId => _groupId;
+
+ String? get id => _id;
+
+ String? get name => _name;
+
+ String? get badgeName => _badgeName;
+
+ num? get quantity => _quantity;
+
+ String? get remark => _remark;
+
+ num? get sort => _sort;
+
+ String? get sourceUrl => _sourceUrl;
+
+ String? get type => _type;
+
+ String? get rewardScene => _rewardScene;
+
+ num? get updateTime => _updateTime;
+
+ Map toJson() {
+ final map = {};
+ map['amount'] = _amount;
+ map['content'] = _content;
+ map['cover'] = _cover;
+ map['createTime'] = _createTime;
+ map['detailType'] = _detailType;
+ map['groupId'] = _groupId;
+ map['id'] = _id;
+ map['name'] = _name;
+ map['badgeName'] = _badgeName;
+ map['quantity'] = _quantity;
+ map['remark'] = _remark;
+ map['sort'] = _sort;
+ map['sourceUrl'] = _sourceUrl;
+ map['type'] = _type;
+ map['rewardScene'] = _rewardScene;
+ map['updateTime'] = _updateTime;
+ return map;
+ }
+}
diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart
index 5292e17..9a02f92 100644
--- a/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart
+++ b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart
@@ -45,7 +45,9 @@ class SCRoomRocketStatusRes {
bool? shake,
List? levels,
SCRoomRocketRewardPreviewRes? rewardPreview,
+ Map? rewardPreviewByLevel,
List? rocketKings,
+ Map>? rocketKingsByLevel,
String? timezone,
int? resetRemainingSeconds,
Object? resetTime,
@@ -75,7 +77,9 @@ class SCRoomRocketStatusRes {
_shake = shake;
_levels = levels;
_rewardPreview = rewardPreview;
+ _rewardPreviewByLevel = rewardPreviewByLevel;
_rocketKings = rocketKings;
+ _rocketKingsByLevel = rocketKingsByLevel;
_timezone = timezone;
_resetRemainingSeconds = resetRemainingSeconds;
_resetTime = resetTime;
@@ -98,7 +102,38 @@ class SCRoomRocketStatusRes {
_energyPercent = _numValue(json['energyPercent']) ?? _displayPercent;
_status = json['status'];
_statusDesc = json['statusDesc'];
- _rocketKings = _kingList(json['rocketKings']);
+ _levels = _levelList(json['levels']);
+ _rewardPreviewByLevel = _rewardPreviewMap(json, _levels);
+ _rewardPreview =
+ SCRoomRocketRewardPreviewRes.tryFromJson(
+ _firstPayload([
+ json['rewardPreview'],
+ json['reward_preview'],
+ json['rewardConfig'],
+ json['reward_config'],
+ json['rewards'],
+ json['rewardList'],
+ ]),
+ ) ??
+ _rewardPreviewForCurrentLevel(
+ _rewardPreviewByLevel,
+ _currentLevel ?? _intValue(_level),
+ );
+ _rocketKingsByLevel = _rocketKingsMap(json, _levels);
+ _rocketKings = _kingList(
+ _firstPayload([
+ json['rocketKings'],
+ json['rocket_kings'],
+ json['kings'],
+ json['podium'],
+ ]),
+ );
+ if ((_rocketKings?.isEmpty ?? true) && _rocketKingsByLevel != null) {
+ _rocketKings = _rocketKingsForCurrentLevel(
+ _rocketKingsByLevel,
+ _currentLevel ?? _intValue(_level),
+ );
+ }
_totalContributors =
json['totalContributors'] ?? (_rocketKings?.length ?? 0);
_claimExpireTime = json['claimExpireTime'];
@@ -107,11 +142,6 @@ class SCRoomRocketStatusRes {
_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'] ??
@@ -180,7 +210,9 @@ class SCRoomRocketStatusRes {
bool? _shake;
List? _levels;
SCRoomRocketRewardPreviewRes? _rewardPreview;
+ Map? _rewardPreviewByLevel;
List? _rocketKings;
+ Map>? _rocketKingsByLevel;
String? _timezone;
int? _resetRemainingSeconds;
Object? _resetTime;
@@ -234,8 +266,14 @@ class SCRoomRocketStatusRes {
SCRoomRocketRewardPreviewRes? get rewardPreview => _rewardPreview;
+ Map? get rewardPreviewByLevel =>
+ _rewardPreviewByLevel;
+
List? get rocketKings => _rocketKings;
+ Map>? get rocketKingsByLevel =>
+ _rocketKingsByLevel;
+
String? get timezone => _timezone;
int? get resetRemainingSeconds => _resetRemainingSeconds;
@@ -244,6 +282,60 @@ class SCRoomRocketStatusRes {
Object? get serverTime => _serverTime;
+ SCRoomRocketRewardPreviewRes? rewardPreviewForLevel(int level) {
+ final normalizedLevel = level.clamp(1, 99).toInt();
+ final byLevel = _rewardPreviewByLevel?[normalizedLevel];
+ if (byLevel != null) {
+ return byLevel;
+ }
+ final levelPreview = _levelFor(normalizedLevel)?.rewardPreview;
+ if (levelPreview != null) {
+ return levelPreview;
+ }
+ final responseLevel = (_level?.toInt() ?? 0).clamp(0, 99).toInt();
+ if (responseLevel > 0) {
+ return normalizedLevel == responseLevel ? _rewardPreview : null;
+ }
+ final current = (_currentLevel ?? 0).clamp(0, 99).toInt();
+ if (current <= 0 || normalizedLevel == current) {
+ return _rewardPreview;
+ }
+ return null;
+ }
+
+ List rocketKingsForLevel(int level) {
+ final normalizedLevel = level.clamp(1, 99).toInt();
+ final byLevel = _rocketKingsByLevel?[normalizedLevel];
+ if (byLevel != null) {
+ return byLevel;
+ }
+ final levelKings = _levelFor(normalizedLevel)?.rocketKings;
+ if (levelKings != null && levelKings.isNotEmpty) {
+ return levelKings;
+ }
+ final responseLevel = (_level?.toInt() ?? 0).clamp(0, 99).toInt();
+ if (responseLevel > 0) {
+ return normalizedLevel == responseLevel
+ ? _rocketKings ?? const []
+ : const [];
+ }
+ final current = (_currentLevel ?? 0).clamp(0, 99).toInt();
+ if (current <= 0 || normalizedLevel == current) {
+ return _rocketKings ?? const [];
+ }
+ return const [];
+ }
+
+ SCRoomRocketLevelRes? _levelFor(int level) {
+ final list = _levels ?? const [];
+ for (final item in list) {
+ if (item.level == level) {
+ return item;
+ }
+ }
+ return null;
+ }
+
Map toJson() {
final map = {};
map['roomId'] = _roomId;
@@ -268,6 +360,19 @@ class SCRoomRocketStatusRes {
map['needEnergy'] = _needEnergy;
map['displayPercent'] = _displayPercent;
map['shake'] = _shake;
+ map['rewardPreviewByLevel'] = _rewardPreviewByLevel?.map(
+ (level, preview) => MapEntry(level.toString(), {
+ 'top1': preview.top1.map((item) => item.toJson()).toList(),
+ 'ignite': preview.ignite.map((item) => item.toJson()).toList(),
+ 'inRoom': preview.inRoom.map((item) => item.toJson()).toList(),
+ }),
+ );
+ map['rocketKingsByLevel'] = _rocketKingsByLevel?.map(
+ (level, users) => MapEntry(
+ level.toString(),
+ users.map((item) => item.toJson()).toList(),
+ ),
+ );
map['timezone'] = _timezone;
map['resetRemainingSeconds'] = _resetRemainingSeconds;
map['resetTime'] = _resetTime;
@@ -335,9 +440,290 @@ class SCRoomRocketStatusRes {
}
static List? _kingList(dynamic value) {
- if (value is! Iterable) {
+ final list = SCRoomRocketKingUserRes.listFromJson(value);
+ return list.isEmpty ? null : list;
+ }
+
+ static SCRoomRocketRewardPreviewRes? _rewardPreviewForCurrentLevel(
+ Map? values,
+ int? currentLevel,
+ ) {
+ if (values == null || values.isEmpty || currentLevel == null) {
return null;
}
- return value.map(SCRoomRocketKingUserRes.fromJson).toList(growable: false);
+ return values[currentLevel];
+ }
+
+ static List? _rocketKingsForCurrentLevel(
+ Map>? values,
+ int? currentLevel,
+ ) {
+ if (values == null || values.isEmpty || currentLevel == null) {
+ return null;
+ }
+ return values[currentLevel];
+ }
+
+ static Map? _rewardPreviewMap(
+ dynamic json,
+ List? levels,
+ ) {
+ final result = {};
+ for (final level in levels ?? const []) {
+ final preview = level.rewardPreview;
+ if (preview != null) {
+ result[level.level] = preview;
+ }
+ }
+ final data = _asMap(json);
+ for (final candidate in [
+ data['rewardPreviewByLevel'],
+ data['rewardPreviewsByLevel'],
+ data['levelRewardPreview'],
+ data['levelRewardPreviews'],
+ data['rewardsByLevel'],
+ data['levelRewards'],
+ data['rocketRewards'],
+ data['rewardPreview'],
+ ]) {
+ _collectRewardPreviewMap(candidate, result);
+ }
+ return result.isEmpty ? null : result;
+ }
+
+ static void _collectRewardPreviewMap(
+ dynamic value,
+ Map result,
+ ) {
+ if (!_hasPayload(value)) {
+ return;
+ }
+ if (value is Iterable && value is! String) {
+ for (final item in value) {
+ final data = _asMap(item);
+ final level = _levelFromValue(
+ _firstPayload([
+ data['level'],
+ data['rocketLevel'],
+ data['levelNo'],
+ data['currentLevel'],
+ ]),
+ );
+ final preview = SCRoomRocketRewardPreviewRes.tryFromJson(
+ _firstPayload([
+ data['rewardPreview'],
+ data['rewardConfig'],
+ data['rewards'],
+ data['rewardList'],
+ data,
+ ]),
+ );
+ if (level != null && preview != null) {
+ result[level] = preview;
+ }
+ }
+ return;
+ }
+ final data = _asMap(value);
+ final singleLevel = _levelFromValue(
+ _firstPayload([
+ data['level'],
+ data['rocketLevel'],
+ data['levelNo'],
+ data['currentLevel'],
+ ]),
+ );
+ if (singleLevel != null) {
+ final preview = SCRoomRocketRewardPreviewRes.tryFromJson(
+ _firstPayload([
+ data['rewardPreview'],
+ data['rewardConfig'],
+ data['rewards'],
+ data['rewardList'],
+ data,
+ ]),
+ );
+ if (preview != null) {
+ result[singleLevel] = preview;
+ }
+ return;
+ }
+ for (final entry in data.entries) {
+ final level = _levelFromMapKey(entry.key);
+ final preview = SCRoomRocketRewardPreviewRes.tryFromJson(entry.value);
+ if (level != null && preview != null) {
+ result[level] = preview;
+ }
+ }
+ }
+
+ static Map>? _rocketKingsMap(
+ dynamic json,
+ List? levels,
+ ) {
+ final result = >{};
+ for (final level in levels ?? const []) {
+ if (level.rocketKings.isNotEmpty) {
+ result[level.level] = level.rocketKings;
+ }
+ }
+ final data = _asMap(json);
+ for (final candidate in [
+ data['rocketKingsByLevel'],
+ data['rocketKingByLevel'],
+ data['levelRocketKings'],
+ data['levelKings'],
+ data['kingsByLevel'],
+ data['podiumByLevel'],
+ data['rocketKings'],
+ ]) {
+ _collectRocketKingsMap(candidate, result);
+ }
+ return result.isEmpty ? null : result;
+ }
+
+ static void _collectRocketKingsMap(
+ dynamic value,
+ Map> result,
+ ) {
+ if (!_hasPayload(value)) {
+ return;
+ }
+ if (value is Iterable && value is! String) {
+ for (final item in value) {
+ final data = _asMap(item);
+ final level = _levelFromValue(
+ _firstPayload([
+ data['level'],
+ data['rocketLevel'],
+ data['levelNo'],
+ data['currentLevel'],
+ ]),
+ );
+ final kings = SCRoomRocketKingUserRes.listFromJson(
+ _firstPayload([
+ data['rocketKings'],
+ data['kings'],
+ data['podium'],
+ data['records'],
+ data['items'],
+ data['list'],
+ ]),
+ );
+ if (level != null && kings.isNotEmpty) {
+ result[level] = kings;
+ }
+ }
+ return;
+ }
+ final data = _asMap(value);
+ final singleLevel = _levelFromValue(
+ _firstPayload([
+ data['level'],
+ data['rocketLevel'],
+ data['levelNo'],
+ data['currentLevel'],
+ ]),
+ );
+ if (singleLevel != null) {
+ final kings = SCRoomRocketKingUserRes.listFromJson(
+ _firstPayload([
+ data['rocketKings'],
+ data['kings'],
+ data['podium'],
+ data['records'],
+ data['items'],
+ data['list'],
+ ]),
+ );
+ if (kings.isNotEmpty) {
+ result[singleLevel] = kings;
+ }
+ return;
+ }
+ for (final entry in data.entries) {
+ final level = _levelFromMapKey(entry.key);
+ final kings = SCRoomRocketKingUserRes.listFromJson(entry.value);
+ if (level != null && kings.isNotEmpty) {
+ result[level] = kings;
+ }
+ }
+ }
+
+ static Map _asMap(dynamic value) {
+ if (value is Map) {
+ return value;
+ }
+ if (value is Map) {
+ return value.map((key, item) => MapEntry(key.toString(), item));
+ }
+ return const {};
+ }
+
+ static dynamic _firstPayload(Iterable values) {
+ for (final value in values) {
+ if (_hasPayload(value)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ static bool _hasPayload(dynamic value) {
+ if (value == null) {
+ return false;
+ }
+ if (value is String) {
+ final text = value.trim();
+ return text.isNotEmpty &&
+ text != '{}' &&
+ text != '[]' &&
+ text.toLowerCase() != 'null';
+ }
+ if (value is Map) {
+ return value.isNotEmpty;
+ }
+ if (value is Iterable) {
+ return value.isNotEmpty;
+ }
+ return true;
+ }
+
+ static int? _levelFromValue(dynamic value) {
+ if (value is int) {
+ return value;
+ }
+ if (value is num) {
+ return value.toInt();
+ }
+ final text = value?.toString().trim() ?? '';
+ final exact = int.tryParse(text);
+ if (exact != null) {
+ return exact;
+ }
+ final match = RegExp(r'\d+').firstMatch(text);
+ if (match == null) {
+ return null;
+ }
+ return int.tryParse(match.group(0) ?? '');
+ }
+
+ static int? _levelFromMapKey(dynamic value) {
+ if (value is int || value is num) {
+ return _levelFromValue(value);
+ }
+ final text = value?.toString().trim() ?? '';
+ final exact = int.tryParse(text);
+ if (exact != null) {
+ return exact;
+ }
+ final match = RegExp(
+ r'^(?:lv|level|rocketLevel|levelNo)[_\-\s.]*([0-9]+)$',
+ caseSensitive: false,
+ ).firstMatch(text);
+ if (match == null) {
+ return null;
+ }
+ return int.tryParse(match.group(1) ?? '');
}
}
diff --git a/lib/shared/business_logic/models/res/sc_rtc_token_res.dart b/lib/shared/business_logic/models/res/sc_rtc_token_res.dart
deleted file mode 100644
index ab784a6..0000000
--- a/lib/shared/business_logic/models/res/sc_rtc_token_res.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-/// newServiceToken : ""
-/// rtcToken : ""
-
-class SCRtcTokenRes {
- SCRtcTokenRes({
- String? newServiceToken,
- String? rtcToken,}){
- _newServiceToken = newServiceToken;
- _rtcToken = rtcToken;
-}
-
- SCRtcTokenRes.fromJson(dynamic json) {
- _newServiceToken = json['newServiceToken'];
- _rtcToken = json['rtcToken'];
- }
- String? _newServiceToken;
- String? _rtcToken;
-SCRtcTokenRes copyWith({ String? newServiceToken,
- String? rtcToken,
-}) => SCRtcTokenRes( newServiceToken: newServiceToken ?? _newServiceToken,
- rtcToken: rtcToken ?? _rtcToken,
-);
- String? get newServiceToken => _newServiceToken;
- String? get rtcToken => _rtcToken;
-
- Map toJson() {
- final map = {};
- map['newServiceToken'] = _newServiceToken;
- map['rtcToken'] = _rtcToken;
- return map;
- }
-
-}
\ No newline at end of file
diff --git a/lib/shared/business_logic/models/res/sc_user_badge_res.dart b/lib/shared/business_logic/models/res/sc_user_badge_res.dart
index e7b6ec9..3e6bfea 100644
--- a/lib/shared/business_logic/models/res/sc_user_badge_res.dart
+++ b/lib/shared/business_logic/models/res/sc_user_badge_res.dart
@@ -59,14 +59,29 @@ class SCUserBadgeRes {
}
return value
.whereType