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() + .where(_isDisplayBadgePayload) .map((item) => WearBadge.fromJson(item)) .where((badge) => _badgeUrl(badge).isNotEmpty) .toList(); } static String _badgeUrl(WearBadge badge) { - return (badge.selectUrl ?? badge.notSelectUrl ?? badge.animationUrl ?? "") - .trim(); + return (badge.selectUrl ?? "").trim(); + } + + static bool _isDisplayBadgePayload(Map item) { + final type = _stringValue(item['type'])?.toUpperCase(); + if (type != null && type != "LONG" && type != "SHORT") { + return false; + } + + final sourceType = _stringValue(item['sourceType'])?.toUpperCase(); + if (sourceType == null) { + return true; + } + return sourceType == "VIP" || + sourceType == "ACTIVITY" || + sourceType == "ACHIEVEMENT"; } static String? _stringValue(dynamic value) { diff --git a/lib/shared/business_logic/repositories/room_repository.dart b/lib/shared/business_logic/repositories/room_repository.dart index f3c09e3..70021ce 100644 --- a/lib/shared/business_logic/repositories/room_repository.dart +++ b/lib/shared/business_logic/repositories/room_repository.dart @@ -202,7 +202,7 @@ abstract class SocialChatRoomRepository { }); ///查询房间火箭状态 - Future rocketStatus(String roomId); + Future rocketStatus(String roomId, {int? level}); ///查询火箭王榜单 Future roomRocketKing( diff --git a/lib/shared/business_logic/repositories/user_repository.dart b/lib/shared/business_logic/repositories/user_repository.dart index b983a3a..730b900 100644 --- a/lib/shared/business_logic/repositories/user_repository.dart +++ b/lib/shared/business_logic/repositories/user_repository.dart @@ -18,7 +18,6 @@ import 'package:yumi/shared/business_logic/models/res/room_black_list_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_res.dart' hide WearBadge; import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_sign_in_reward_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; @@ -78,13 +77,6 @@ abstract class SocialChatUserRepository { ///上麦心跳 Future anchorHeartbeat(String roomId); - ///声网token - Future getRtcToken( - String channel, - String userId, { - bool isPublisher = false, - }); - ///退出房间 Future quitRoom(String roomId); diff --git a/lib/shared/data_sources/sources/remote/net/network_client.dart b/lib/shared/data_sources/sources/remote/net/network_client.dart index ed9dfd4..b0e893d 100644 --- a/lib/shared/data_sources/sources/remote/net/network_client.dart +++ b/lib/shared/data_sources/sources/remote/net/network_client.dart @@ -161,8 +161,6 @@ const Map _localRouteOverrides = { "/room/live-voice/entry", "58f9485b5b9f401c6a14bc1e6534122c7a085bb5f6a613094188ca56fe8bc03d": "/live/user/heartbeat", - "2572c69d25a0946b0cb35b24bdfcd61c9bcef4afdaced7dfc47a641bd9584f96": - "/external/im-trtc/tmp/agora/sig", "3e451c31ab651a577a3b7796b32041e7": "/live/room/quit", "2fa1d4f56bb726558904ce2a50b83f961e1cf9b945681bdfb3d4cf7a5ecfc943": "/user/user-profile", @@ -357,7 +355,7 @@ class ApiInterceptor extends Interceptor { // MaterialPageRoute(builder: (_) { // Provider.of(_, listen: false).destoryRtm(); // Provider.of(_, listen: false).logout(); - // Provider.of(_, listen: false).engine?.leaveChannel(); + // Provider.of(_, listen: false).releaseRtcEngineForAppTermination(); // return const LoginPage(); // }), // (route) => false // 清除所有路由 diff --git a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart index fe5c4c1..782880d 100644 --- a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart @@ -1001,9 +1001,15 @@ class SCChatRoomRepository implements SocialChatRoomRepository { ///go/app/voice-room/rocket/status @override - Future rocketStatus(String roomId) async { + Future rocketStatus( + String roomId, { + int? level, + }) async { Map params = {}; params["roomId"] = roomId; + if (level != null && level > 0) { + params["level"] = level; + } const path = "/go/app/voice-room/rocket/status"; _rocketApiDebug('request GET $path', query: params); final result = await http.get( diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart index 2d527ca..2226340 100644 --- a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -9,7 +9,6 @@ import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_record_list import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_res.dart' hide WearBadge; -import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart'; @@ -228,25 +227,6 @@ class SCAccountRepository implements SocialChatUserRepository { return result; } - ///external/im-trtc/tmp/agora/sig - @override - Future getRtcToken( - String channel, - String userId, { - bool isPublisher = false, - }) async { - final result = await http.get( - "2572c69d25a0946b0cb35b24bdfcd61c9bcef4afdaced7dfc47a641bd9584f96", - queryParams: { - "channelName": channel, - "userId": userId, - "isPublisher": isPublisher, - }, - fromJson: (json) => SCRtcTokenRes.fromJson(json), - ); - return result; - } - ///live/room/quit @override Future quitRoom(String roomId) async { diff --git a/lib/shared/tools/sc_version_utils.dart b/lib/shared/tools/sc_version_utils.dart index 054c920..8739d6d 100644 --- a/lib/shared/tools/sc_version_utils.dart +++ b/lib/shared/tools/sc_version_utils.dart @@ -528,7 +528,7 @@ class _UpdateNotice extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: const Text( - 'This Agora-based version has expired. Please update to the latest ' + 'This RTC version has expired. Please update to the latest ' 'version to continue using voice rooms normally.', textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart b/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart index 0ee187f..c1578bd 100644 --- a/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart +++ b/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart @@ -163,8 +163,7 @@ class _SCUserBadgeStripState extends State { } String _badgeUrl(WearBadge badge) { - return (badge.selectUrl ?? badge.notSelectUrl ?? badge.animationUrl ?? "") - .trim(); + return (badge.selectUrl ?? "").trim(); } List _withSpacing(List children, double spacing) { diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart index 456c929..6840f12 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart @@ -1,5 +1,6 @@ import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart'; @@ -42,9 +43,12 @@ class RoomRocketApiMapper { } static List? rewardsFromStatus( - SCRoomRocketStatusRes? status, - ) { - final rewards = status?.rewardPreview?.all ?? const []; + SCRoomRocketStatusRes? status, { + int? level, + }) { + final rewards = + status?.rewardPreviewForLevel(level ?? status.currentLevel ?? 1)?.all ?? + const []; if (rewards.isEmpty) { return null; } @@ -52,9 +56,18 @@ class RoomRocketApiMapper { } static RoomRocketRewardGroups? rewardGroupsFromStatus( - SCRoomRocketStatusRes? status, + SCRoomRocketStatusRes? status, { + int? level, + }) { + final preview = status?.rewardPreviewForLevel( + level ?? status.currentLevel ?? 1, + ); + return rewardGroupsFromPreview(preview); + } + + static RoomRocketRewardGroups? rewardGroupsFromPreview( + SCRoomRocketRewardPreviewRes? preview, ) { - final preview = status?.rewardPreview; if (preview == null) { return null; } @@ -65,6 +78,44 @@ class RoomRocketApiMapper { ); } + static Map rewardGroupsByLevelFromConfigs( + List configs, + ) { + final result = {}; + for (final config in configs) { + final level = config.level?.toInt(); + if (level == null || level <= 0) { + continue; + } + final groups = rewardGroupsFromConfig(config); + if (groups != null) { + result[level] = groups; + } + } + return result; + } + + static RoomRocketRewardGroups? rewardGroupsFromConfig( + SCRoomRocketConfigRes config, + ) { + final explicitTop1 = _rewardItemsFromConfig(config.rewardConfig); + final explicitIgnite = _rewardItemsFromConfig(config.igniteRewardConfig); + final explicitInRoom = _rewardItemsFromConfig(config.inRoomRewardConfig); + final normal = _rewardItemsFromConfig(config.normalRewardConfig); + final top1 = [...explicitTop1]; + final setOff = [...explicitIgnite]; + final inRoom = [...explicitInRoom, ...normal]; + final groups = RoomRocketRewardGroups( + top1: top1, + setOff: setOff, + inRoom: inRoom, + ); + if (groups.top1.isEmpty && groups.setOff.isEmpty && groups.inRoom.isEmpty) { + return null; + } + return groups; + } + static RoomRocketRewardItem rewardItemFromPreview( SCRoomRocketRewardRes reward, ) { @@ -76,14 +127,89 @@ class RoomRocketApiMapper { ); } - static List? crewFromStatus( - SCRoomRocketStatusRes? status, + static List _rewardItemsFromConfig( + List? configs, + ) { + final source = configs ?? const []; + if (source.isEmpty) { + return const []; + } + final sorted = [...source]..sort((a, b) { + final aSort = a.sort ?? 0; + final bSort = b.sort ?? 0; + return aSort.compareTo(bSort); + }); + return sorted.map(_rewardItemFromConfig).toList(growable: false); + } + + static RoomRocketRewardItem _rewardItemFromConfig(RewardConfig config) { + final rawType = (config.type ?? '').trim(); + final detailType = (config.detailType ?? '').trim(); + final type = + detailType.toUpperCase() == 'GOLD' + ? detailType + : (rawType.isEmpty ? detailType : rawType); + final amount = _rewardAmountText(config); + final cover = (config.cover ?? config.sourceUrl ?? '').trim(); + return RoomRocketRewardItem( + imageUrl: cover.isEmpty ? null : cover, + amount: amount, + rewardType: type.isEmpty ? 'PROPS' : type, + expireDays: (config.quantity ?? 0).toInt(), + ); + } + + static String _rewardAmountText(RewardConfig config) { + final type = (config.type ?? '').trim().toUpperCase(); + final detailType = (config.detailType ?? '').trim().toUpperCase(); + if (type == 'GOLD' || detailType == 'GOLD') { + final amount = config.amount ?? num.tryParse(config.content ?? ''); + if (amount == null || amount <= 0) { + return '0'; + } + return amount % 1 == 0 ? amount.toInt().toString() : amount.toString(); + } + final quantity = config.quantity ?? 0; + if (quantity > 0) { + return quantity.toInt().toString(); + } + final name = + (config.name ?? + config.badgeName ?? + config.remark ?? + config.content ?? + '') + .trim(); + return name.isEmpty ? '1' : name; + } + + static List? crewFromStatus( + SCRoomRocketStatusRes? status, { + int? level, + }) { + final kings = status?.rocketKingsForLevel( + level ?? status.currentLevel ?? 1, + ); + return crewFromKingUsers(kings ?? const []); + } + + static List crewFromKing(SCRoomRocketKingRes king) { + final source = king.podium.isNotEmpty ? king.podium : king.displayRecords; + return crewFromKingUsers(source); + } + + static List crewFromKingUsers( + List kings, ) { - final kings = status?.rocketKings ?? const []; if (kings.isEmpty) { return const []; } - return kings + final sorted = [...kings]..sort((a, b) { + final aRank = a.rank <= 0 ? 999 : a.rank; + final bRank = b.rank <= 0 ? 999 : b.rank; + return aRank.compareTo(bRank); + }); + return sorted .take(3) .map( (item) => RoomRocketCrewMember( diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart index aff1d6d..a0eaced 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart @@ -172,15 +172,13 @@ class RoomRocketDialog extends StatelessWidget { ) { final hasNoContributors = status != null && (status.totalContributors ?? 0) <= 0; - if (hasNoContributors) { - return const []; - } - final source = - crew?.where((item) => item.asset != null || item.imageUrl != null) ?? - const []; + final source = crew ?? const []; if (source.isNotEmpty) { return source.take(3).toList(); } + if (hasNoContributors) { + return const []; + } return const []; } @@ -376,6 +374,7 @@ class _RocketCanvas extends StatelessWidget { _buildResetTime(), _buildRewardPanel(), _buildBottomStrip(), + _buildCloseButton(), ], ); } @@ -563,6 +562,40 @@ class _RocketCanvas extends StatelessWidget { ); } + Widget _buildCloseButton() { + return Positioned( + left: s(337), + top: s(94), + width: s(30), + height: s(30), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onClose, + child: Center( + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFF75F3CB).withValues(alpha: 0.82), + width: s(1), + ), + color: const Color(0xFF002F26).withValues(alpha: 0.35), + ), + child: SizedBox( + width: s(20), + height: s(20), + child: Icon( + Icons.close_rounded, + size: s(17), + color: const Color(0xFF75F3CB), + ), + ), + ), + ), + ), + ); + } + Widget _buildResetTime() { return Stack( children: [ @@ -916,11 +949,18 @@ class _CrewAvatar extends StatelessWidget { child: SizedBox( width: 42 * scale, height: 42 * scale, - child: _RocketImage( - asset: member.asset, - imageUrl: member.imageUrl, - fit: BoxFit.cover, - ), + child: + member.asset != null || member.imageUrl != null + ? _RocketImage( + asset: member.asset, + imageUrl: member.imageUrl, + fit: BoxFit.cover, + ) + : Image.asset( + _RocketAssets.defaultAvatar, + fit: BoxFit.cover, + gaplessPlayback: true, + ), ), ), ], @@ -957,9 +997,15 @@ class _RocketImage extends StatelessWidget { ); } if (RoomRocketPagEffectOverlay.isPag(source)) { + RoomRocketPagEffectOverlay.debugPagResource( + source, + place: debugName ?? 'dialog-image-network', + ); return RoomRocketPagPreview( resource: source, loop: true, + fit: fit, + underlayBuilder: (_) => _imageFallback(), defaultBuilder: (_) => const SCRotatingDotsLoading(), ); } @@ -1000,9 +1046,15 @@ class _RocketImage extends StatelessWidget { ); } if (RoomRocketPagEffectOverlay.isPag(source)) { + RoomRocketPagEffectOverlay.debugPagResource( + source, + place: debugName ?? 'dialog-image-asset', + ); return RoomRocketPagPreview( resource: source, loop: true, + fit: fit, + underlayBuilder: (_) => _imageFallback(), defaultBuilder: (_) => _imageFallback(), ); } @@ -1061,7 +1113,11 @@ class _RocketImage extends StatelessWidget { } Widget _imageFallback() { - if (asset != null && asset!.trim().isNotEmpty && !_isSvg(asset!)) { + if (asset != null && + asset!.trim().isNotEmpty && + !_isSvg(asset!) && + !RoomRocketPagEffectOverlay.isPag(asset!) && + !SCNetworkSvgaWidget.isSvga(asset!)) { return Image.asset(asset!, fit: fit, gaplessPlayback: true); } return const SizedBox.shrink(); @@ -1691,6 +1747,8 @@ class _RocketAssets { static const String rewardBottom = 'sc_images/room/rocket/reward_dialog_bottom.png'; static const String rewardThumb = 'sc_images/room/rocket/reward_thumb.png'; + static const String defaultAvatar = + 'sc_images/general/sc_icon_avar_defalt.png'; static const String coin = 'sc_images/register_reward/yumi_coin.png'; } diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart index e5f089c..07db494 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart @@ -44,6 +44,7 @@ class RoomRocketFloatingEntry extends StatelessWidget { child: _RoomRocketEntryButton( iconUrl: level?.rocketIconUrl ?? '', progressUrl: level?.progressBarUrl ?? '', + progressPercent: percent, shake: shouldShake, loading: status == null, ), @@ -111,12 +112,14 @@ class _RoomRocketEntryButton extends StatefulWidget { const _RoomRocketEntryButton({ required this.iconUrl, required this.progressUrl, + required this.progressPercent, required this.shake, required this.loading, }); final String iconUrl; final String progressUrl; + final double progressPercent; final bool shake; final bool loading; @@ -197,7 +200,7 @@ class _RoomRocketEntryButtonState extends State<_RoomRocketEntryButton> height: 8.w, child: widget.progressUrl.trim().isEmpty - ? const SizedBox.shrink() + ? _RocketEntryProgressBar(percent: widget.progressPercent) : ClipRRect( borderRadius: BorderRadius.circular(5.w), child: _RocketEntryImage( @@ -214,6 +217,44 @@ class _RoomRocketEntryButtonState extends State<_RoomRocketEntryButton> } } +class _RocketEntryProgressBar extends StatelessWidget { + const _RocketEntryProgressBar({required this.percent}); + + final double percent; + + @override + Widget build(BuildContext context) { + final value = (percent / 100).clamp(0.0, 1.0).toDouble(); + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.w), + color: const Color(0xFF082D27).withValues(alpha: 0.86), + border: Border.all( + color: const Color(0xFF75F3CB).withValues(alpha: 0.52), + width: 0.8.w, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(5.w), + child: Align( + alignment: Alignment.centerLeft, + child: FractionallySizedBox( + widthFactor: value, + heightFactor: 1, + child: const DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFFB8FFE6), Color(0xFF1BD091)], + ), + ), + ), + ), + ), + ), + ); + } +} + class _RocketEntryImage extends StatelessWidget { const _RocketEntryImage({ required this.source, @@ -232,9 +273,14 @@ class _RocketEntryImage extends StatelessWidget { return _placeholder(); } if (RoomRocketPagEffectOverlay.isPag(value)) { + RoomRocketPagEffectOverlay.debugPagResource( + value, + place: 'floating-entry', + ); return RoomRocketPagPreview( resource: value, loop: true, + fit: fit, defaultBuilder: (_) => _placeholder(), ); } @@ -305,6 +351,11 @@ class _RoomRocketDialogLoader extends StatefulWidget { class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { SCRoomRocketStatusRes? _status; List? _noteSections; + Map _configRewardGroupsByLevel = {}; + final Map _statusRewardGroupsByLevel = {}; + final Set _loadingRewardLevels = {}; + final Map> _crewByLevel = {}; + final Set _loadingCrewLevels = {}; int _selectedLevel = 0; RoomRocketRewardTab _selectedRewardTab = RoomRocketRewardTab.top1; bool _levelTouched = false; @@ -314,7 +365,10 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { super.initState(); _status = widget.initialStatus; _selectedLevel = _levelIndexFromStatus(widget.initialStatus); + _cacheRewardGroupsFromStatus(widget.initialStatus, _selectedLevel + 1); unawaited(_load()); + unawaited(_loadRewardForLevel(_selectedLevel + 1)); + unawaited(_loadConfigs()); unawaited(_loadRule()); } @@ -335,7 +389,10 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { if (!_levelTouched) { _selectedLevel = _levelIndexFromStatus(res); } + _cacheRewardGroupsFromStatus(res, _selectedLevel + 1); }); + unawaited(_loadCrewForLevel(_selectedLevel + 1)); + unawaited(_loadRewardForLevel(_selectedLevel + 1)); try { Provider.of( context, @@ -362,20 +419,65 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { } } + Future _loadConfigs() async { + try { + _rocketUiDebug('request enabled rocket configs'); + final configs = await SCChatRoomRepository().rocketConfigEnabled(); + if (!mounted) { + return; + } + final groups = RoomRocketApiMapper.rewardGroupsByLevelFromConfigs( + configs, + ); + setState(() { + _configRewardGroupsByLevel = groups; + }); + _rocketUiDebug( + 'rocket configs loaded rewardLevels=${groups.keys.toList()}', + ); + } catch (error) { + _rocketUiDebug('rocket configs request failed: $error'); + } + } + @override Widget build(BuildContext context) { final status = _status; final selectedLevel = _selectedLevel.clamp(0, 5).toInt(); + final selectedLevelNumber = selectedLevel + 1; + final currentLevelNumber = _levelNumberFromStatus(status); + final statusRewardGroups = RoomRocketApiMapper.rewardGroupsFromStatus( + status, + level: selectedLevelNumber, + ); + final liveRewardGroups = _statusRewardGroupsByLevel[selectedLevelNumber]; + final configRewardGroups = _configRewardGroupsByLevel[selectedLevelNumber]; + final rewardGroupsFromStatus = liveRewardGroups ?? statusRewardGroups; + final rewardGroups = + selectedLevelNumber == currentLevelNumber + ? _mergeRewardGroups(rewardGroupsFromStatus, configRewardGroups) + : rewardGroupsFromStatus ?? const RoomRocketRewardGroups(); return RoomRocketDialog( status: status, levels: RoomRocketApiMapper.levelsFromStatus(status), selectedLevel: selectedLevel, onLevelSelected: _onLevelSelected, - rewards: RoomRocketApiMapper.rewardsFromStatus(status), - rewardGroups: RoomRocketApiMapper.rewardGroupsFromStatus(status), + rewards: + rewardGroups == null + ? RoomRocketApiMapper.rewardsFromStatus( + status, + level: selectedLevelNumber, + ) + : null, + rewardGroups: rewardGroups, selectedRewardTab: _selectedRewardTab, onRewardTabSelected: _onRewardTabSelected, - crew: RoomRocketApiMapper.crewFromStatus(status), + crew: + _crewByLevel[selectedLevelNumber] ?? + RoomRocketApiMapper.crewFromStatus( + status, + level: selectedLevelNumber, + ), onClose: widget.onClose, onOpenRank: () => _openRank(selectedLevel), onOpenRecord: _openRecord, @@ -383,12 +485,45 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { ); } + RoomRocketRewardGroups? _mergeRewardGroups( + RoomRocketRewardGroups? primary, + RoomRocketRewardGroups? fallback, + ) { + if (primary == null) { + return fallback; + } + if (fallback == null) { + return primary; + } + return RoomRocketRewardGroups( + top1: primary.top1.isNotEmpty ? primary.top1 : fallback.top1, + setOff: primary.setOff.isNotEmpty ? primary.setOff : fallback.setOff, + inRoom: primary.inRoom.isNotEmpty ? primary.inRoom : fallback.inRoom, + ); + } + int _levelIndexFromStatus(SCRoomRocketStatusRes? status) { return ((status?.currentLevel ?? status?.level ?? 1) - 1) .clamp(0, 5) .toInt(); } + int _levelNumberFromStatus(SCRoomRocketStatusRes? status) { + return (status?.currentLevel ?? status?.level ?? 1).clamp(1, 99).toInt(); + } + + void _cacheRewardGroupsFromStatus(SCRoomRocketStatusRes? status, int level) { + final normalizedLevel = level.clamp(1, 99).toInt(); + final groups = RoomRocketApiMapper.rewardGroupsFromStatus( + status, + level: normalizedLevel, + ); + if (groups == null) { + return; + } + _statusRewardGroupsByLevel[normalizedLevel] = groups; + } + void _debugStatusAssets(SCRoomRocketStatusRes status) { final levels = status.levels ?? const []; _rocketUiDebug( @@ -407,11 +542,133 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { } void _onLevelSelected(int index) { - _rocketUiDebug('select rocket level=${index + 1}'); + final selectedIndex = index.clamp(0, 5).toInt(); + _rocketUiDebug('select rocket level=${selectedIndex + 1}'); setState(() { _levelTouched = true; - _selectedLevel = index.clamp(0, 5).toInt(); + _selectedLevel = selectedIndex; }); + unawaited(_loadRewardForLevel(selectedIndex + 1)); + unawaited(_loadCrewForLevel(selectedIndex + 1)); + } + + Future _loadRewardForLevel(int level) async { + final normalizedLevel = level.clamp(1, 99).toInt(); + if (_statusRewardGroupsByLevel.containsKey(normalizedLevel) || + _loadingRewardLevels.contains(normalizedLevel)) { + return; + } + if (widget.roomId.trim().isEmpty) { + return; + } + setState(() { + _loadingRewardLevels.add(normalizedLevel); + }); + try { + _rocketUiDebug( + 'request dialog rewards roomId=${widget.roomId} level=$normalizedLevel', + ); + final res = await SCChatRoomRepository().rocketStatus( + widget.roomId, + level: normalizedLevel, + ); + if (!mounted) { + return; + } + final groups = + RoomRocketApiMapper.rewardGroupsFromStatus( + res, + level: normalizedLevel, + ) ?? + (_statusResponseMatchesLevel(res, normalizedLevel) + ? RoomRocketApiMapper.rewardGroupsFromPreview(res.rewardPreview) + : null); + if (groups == null) { + _rocketUiDebug('dialog rewards empty level=$normalizedLevel'); + return; + } + setState(() { + _statusRewardGroupsByLevel[normalizedLevel] = groups; + }); + _rocketUiDebug( + 'dialog rewards loaded level=$normalizedLevel ' + 'top1=${groups.top1.length} setOff=${groups.setOff.length} ' + 'inRoom=${groups.inRoom.length}', + ); + } catch (error) { + _rocketUiDebug( + 'dialog rewards request failed level=$normalizedLevel error=$error', + ); + } finally { + if (mounted) { + setState(() { + _loadingRewardLevels.remove(normalizedLevel); + }); + } else { + _loadingRewardLevels.remove(normalizedLevel); + } + } + } + + bool _statusResponseMatchesLevel(SCRoomRocketStatusRes status, int level) { + final normalizedLevel = level.clamp(1, 99).toInt(); + final byLevel = status.rewardPreviewByLevel; + if (byLevel != null && byLevel.containsKey(normalizedLevel)) { + return true; + } + return status.level?.toInt() == normalizedLevel || + status.currentLevel == normalizedLevel; + } + + Future _loadCrewForLevel(int level) async { + final normalizedLevel = level.clamp(1, 99).toInt(); + if (_crewByLevel.containsKey(normalizedLevel) || + _loadingCrewLevels.contains(normalizedLevel)) { + return; + } + if (widget.roomId.trim().isEmpty) { + return; + } + setState(() { + _loadingCrewLevels.add(normalizedLevel); + }); + try { + _rocketUiDebug( + 'request dialog king avatars roomId=${widget.roomId} level=$normalizedLevel', + ); + final res = await SCChatRoomRepository().roomRocketKing( + widget.roomId, + level: normalizedLevel, + limit: 3, + ); + if (!mounted) { + return; + } + final crew = RoomRocketApiMapper.crewFromKing(res); + setState(() { + _crewByLevel[normalizedLevel] = crew; + }); + _rocketUiDebug( + 'dialog king avatars loaded level=$normalizedLevel count=${crew.length}', + ); + } catch (error) { + _rocketUiDebug( + 'dialog king avatars request failed level=$normalizedLevel error=$error', + ); + if (mounted) { + setState(() { + _crewByLevel[normalizedLevel] = const []; + }); + } + } finally { + if (mounted) { + setState(() { + _loadingCrewLevels.remove(normalizedLevel); + }); + } else { + _loadingCrewLevels.remove(normalizedLevel); + } + } } void _onRewardTabSelected(RoomRocketRewardTab tab) { diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart index 34c31eb..0c618ad 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -22,6 +24,19 @@ class RoomRocketPagEffectOverlay extends StatefulWidget { return lower.contains('.pag'); } + static void debugPagResource(String resource, {required String place}) { + if (!kDebugMode) { + return; + } + final value = resource.trim(); + debugPrint( + '[RoomRocketPag] detected place=$place ' + 'kind=${_pagSourceKind(value)} ' + 'isPag=${isPag(value)} ' + 'resource=${_shortPagResource(value)}', + ); + } + @override State createState() => _RoomRocketPagEffectOverlayState(); @@ -55,6 +70,7 @@ class _RoomRocketPagEffectOverlayState width: size.width, height: size.height, loop: false, + fit: BoxFit.cover, onCompleted: _complete, defaultBuilder: (_) => const Center(child: SCRotatingDotsLoading()), @@ -117,6 +133,8 @@ class RoomRocketPagPreview extends StatelessWidget { this.width, this.height, this.loop = true, + this.fit = BoxFit.contain, + this.underlayBuilder, this.defaultBuilder, }); @@ -124,6 +142,8 @@ class RoomRocketPagPreview extends StatelessWidget { final double? width; final double? height; final bool loop; + final BoxFit fit; + final WidgetBuilder? underlayBuilder; final WidgetBuilder? defaultBuilder; @override @@ -141,6 +161,8 @@ class RoomRocketPagPreview extends StatelessWidget { width: resolvedWidth, height: resolvedHeight, loop: loop, + fit: fit, + underlayBuilder: underlayBuilder, defaultBuilder: defaultBuilder ?? (_) => const Center(child: SCRotatingDotsLoading()), @@ -156,6 +178,8 @@ class _RoomRocketPagView extends StatefulWidget { this.width, this.height, required this.loop, + required this.fit, + this.underlayBuilder, this.onCompleted, this.defaultBuilder, }); @@ -164,6 +188,8 @@ class _RoomRocketPagView extends StatefulWidget { final double? width; final double? height; final bool loop; + final BoxFit fit; + final WidgetBuilder? underlayBuilder; final VoidCallback? onCompleted; final WidgetBuilder? defaultBuilder; @@ -172,22 +198,49 @@ class _RoomRocketPagView extends StatefulWidget { } class _RoomRocketPagViewState extends State<_RoomRocketPagView> { - late Future _bytesFuture; + static int _nextDebugId = 1; + + final int _debugId = _nextDebugId++; + late GlobalKey _pagKey; + Uint8List? _pagBytes; + bool _initialized = false; + bool _loadFailed = false; + Timer? _startWatchdog; @override void initState() { super.initState(); - _bytesFuture = _loadPagBytes(widget.resource.trim()); + _pagKey = GlobalKey(); + _debug('mount'); + _prepareResource(); } @override void didUpdateWidget(covariant _RoomRocketPagView oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.resource.trim() != widget.resource.trim()) { - _bytesFuture = _loadPagBytes(widget.resource.trim()); + _pagKey = GlobalKey(); + _pagBytes = null; + _initialized = false; + _loadFailed = false; + _debug( + 'resource changed old=${_shortPagResource(oldWidget.resource)} ' + 'new=${_shortPagResource(widget.resource)}', + ); + _prepareResource(); } } + @override + void dispose() { + _debug( + 'dispose initialized=$_initialized ' + 'hasBytes=${_pagBytes != null} loadFailed=$_loadFailed', + ); + _startWatchdog?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { final path = widget.resource.trim(); @@ -199,45 +252,281 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> { final defaultChild = widget.defaultBuilder?.call(context) ?? const Center(child: SCRotatingDotsLoading()); - return FutureBuilder( - future: _bytesFuture, - builder: (context, snapshot) { - final bytes = snapshot.data; - if (snapshot.connectionState != ConnectionState.done || - bytes == null || - bytes.isEmpty) { - return defaultChild; - } - return PAGView.bytes( - bytes, - key: ValueKey('room-rocket-pag:$path:${widget.loop}'), - width: widget.width, - height: widget.height, - autoPlay: true, - repeatCount: repeatCount, - onAnimationEnd: widget.onCompleted, - defaultBuilder: (_) => defaultChild, - ); - }, + final underlay = widget.underlayBuilder?.call(context); + if (_loadFailed) { + return _withUnderlay(underlay, defaultChild); + } + final bytes = _pagBytes; + if (bytes == null) { + return _withUnderlay(underlay, defaultChild); + } + final pagView = PAGView.bytes( + bytes, + key: _pagKey, + width: null, + height: null, + autoPlay: false, + repeatCount: repeatCount, + onInit: _handleInit, + onAnimationStart: _handleAnimationStart, + onAnimationEnd: _handleAnimationEnd, + onAnimationCancel: _handleAnimationCancel, + onAnimationRepeat: _handleAnimationRepeat, + defaultBuilder: (_) => const SizedBox(width: 1, height: 1), + ); + return SizedBox( + width: widget.width, + height: widget.height, + child: ClipRect( + child: Stack( + fit: StackFit.expand, + children: [ + if (underlay != null) underlay, + FittedBox(fit: widget.fit, child: pagView), + if (!_initialized) Positioned.fill(child: defaultChild), + ], + ), + ), ); } - Future _loadPagBytes(String path) async { + void _prepareResource() { + final path = widget.resource.trim(); + _debug( + 'prepare kind=${_pagSourceKind(path)} ' + 'isPag=${RoomRocketPagEffectOverlay.isPag(path)}', + ); + _restartWatchdog(); + unawaited(_loadBytes(path)); + } + + Future _loadBytes(String path) async { + final stopwatch = Stopwatch()..start(); try { - final data = - _isNetwork(path) - ? await NetworkAssetBundle(Uri.parse(path)).load(path) - : await rootBundle.load(path); - return data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - } catch (error) { - if (kDebugMode) { - debugPrint('[RoomRocketPag] load failed path=$path error=$error'); + final isNetwork = _isPagNetwork(path); + _debug( + '${isNetwork ? 'download' : 'asset load'} start ' + 'host=${_pagHost(path)}', + ); + final data = await (isNetwork + ? NetworkAssetBundle(Uri.parse(path)).load(path) + : rootBundle.load(path)) + .timeout(const Duration(seconds: 15)); + final bytes = Uint8List.fromList( + data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes), + ); + if (!mounted || widget.resource.trim() != path) { + _debug( + 'bytes ignored stale mounted=$mounted ' + 'elapsedMs=${stopwatch.elapsedMilliseconds}', + ); + return; } - return null; + if (bytes.isEmpty) { + _markLoadFailed('download empty'); + return; + } + _debug( + 'bytes ready bytes=${bytes.length} ' + 'head=${_pagByteHead(bytes)} ' + 'elapsedMs=${stopwatch.elapsedMilliseconds}', + ); + setState(() { + _pagBytes = bytes; + }); + _restartWatchdog(); + } catch (error) { + if (!mounted || widget.resource.trim() != path) { + return; + } + _markLoadFailed( + 'bytes load failed elapsedMs=${stopwatch.elapsedMilliseconds}: $error', + ); } } - static bool _isNetwork(String value) { - return value.startsWith('http://') || value.startsWith('https://'); + void _handleInit() { + _debug('init'); + _startWatchdog?.cancel(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + final state = _pagKey.currentState; + if (state == null) { + _markLoadFailed('init failed state=null'); + return; + } + final rawWidth = state.rawWidth; + final rawHeight = state.rawHeight; + if (rawWidth <= 0 || rawHeight <= 0) { + _markLoadFailed('init failed rawSize=${rawWidth}x$rawHeight'); + return; + } + _debug( + 'ready rawSize=${rawWidth}x$rawHeight ' + 'viewSize=${widget.width?.toStringAsFixed(1)}x' + '${widget.height?.toStringAsFixed(1)} fit=${widget.fit.name}', + ); + unawaited(_debugLayers(state, rawWidth, rawHeight)); + if (!_initialized) { + setState(() => _initialized = true); + } + try { + _debug('start requested'); + state.start(); + } catch (error) { + _markLoadFailed('start threw: $error'); + } + }); + } + + void _handleAnimationStart() { + _debug('start'); + } + + void _handleAnimationEnd() { + _debug('end'); + widget.onCompleted?.call(); + } + + void _handleAnimationCancel() { + _debug('cancel'); + } + + void _handleAnimationRepeat() { + _debug('repeat'); + } + + Future _debugLayers( + PAGViewState state, + double rawWidth, + double rawHeight, + ) async { + if (!kDebugMode) { + return; + } + final points = { + 'center': Offset(rawWidth / 2, rawHeight / 2), + 'upper': Offset(rawWidth / 2, rawHeight * 0.25), + 'lower': Offset(rawWidth / 2, rawHeight * 0.75), + }; + for (final entry in points.entries) { + try { + final layers = await state.getLayersUnderPoint( + entry.value.dx, + entry.value.dy, + ); + _debug( + 'layers point=${entry.key} ' + 'x=${entry.value.dx.toStringAsFixed(1)} ' + 'y=${entry.value.dy.toStringAsFixed(1)} ' + 'count=${layers.length} names=$layers', + ); + } catch (error) { + _debug('layers point=${entry.key} failed: $error'); + } + } + } + + void _restartWatchdog() { + _startWatchdog?.cancel(); + _debug( + 'watchdog armed initialized=$_initialized hasBytes=${_pagBytes != null}', + ); + _startWatchdog = Timer(const Duration(seconds: 8), () { + if (mounted && !_initialized) { + if (_pagBytes == null && !_loadFailed) { + _debug('waiting for pag bytes'); + return; + } + _markLoadFailed('waiting for pag init'); + } + }); + } + + Widget _withUnderlay(Widget? underlay, Widget foreground) { + if (underlay == null) { + return foreground; + } + return SizedBox( + width: widget.width, + height: widget.height, + child: Stack( + fit: StackFit.expand, + children: [underlay, Positioned.fill(child: foreground)], + ), + ); + } + + void _markLoadFailed(String reason) { + _debug('failed reason=$reason'); + _startWatchdog?.cancel(); + if (mounted) { + setState(() { + _initialized = false; + _loadFailed = true; + }); + } else { + _loadFailed = true; + } + if (!widget.loop) { + widget.onCompleted?.call(); + } + } + + void _debug(String event) { + if (!kDebugMode) { + return; + } + debugPrint( + '[RoomRocketPag#$_debugId] $event ' + 'loop=${widget.loop} ' + 'size=${widget.width?.toStringAsFixed(1)}x' + '${widget.height?.toStringAsFixed(1)} ' + 'kind=${_pagSourceKind(widget.resource)} ' + 'resource=${_shortPagResource(widget.resource)}', + ); } } + +bool _isPagNetwork(String value) { + return value.startsWith('http://') || value.startsWith('https://'); +} + +String _pagSourceKind(String value) { + final text = value.trim(); + if (text.isEmpty) { + return 'empty'; + } + if (_isPagNetwork(text)) { + return 'network'; + } + if (text.startsWith('sc_images/') || text.startsWith('assets/')) { + return 'asset'; + } + return 'local-or-unknown'; +} + +String _pagHost(String value) { + if (!_isPagNetwork(value)) { + return '-'; + } + return Uri.tryParse(value)?.host ?? '-'; +} + +String _shortPagResource(String value) { + final text = value.trim(); + if (text.length <= 220) { + return text; + } + return '${text.substring(0, 120)}...${text.substring(text.length - 80)}'; +} + +String _pagByteHead(Uint8List bytes) { + final length = bytes.length < 12 ? bytes.length : 12; + return bytes + .take(length) + .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + .join(' '); +} diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart index b9db84e..227324b 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart @@ -320,7 +320,7 @@ class _RewardDialogCanvas extends StatelessWidget { top: s(482), width: s(_designWidth), child: _SectionTitle( - text: '12 users received rewards', + text: _receivedRewardsText(winners.length), scale: scale, ), ), @@ -329,6 +329,11 @@ class _RewardDialogCanvas extends StatelessWidget { ); } + String _receivedRewardsText(int count) { + final unit = count == 1 ? 'user' : 'users'; + return '$count $unit received rewards'; + } + Widget _buildEmptyState() { return Positioned( left: s(0), diff --git a/lib/ui_kit/widgets/room/room_msg_item.dart b/lib/ui_kit/widgets/room/room_msg_item.dart index 321ed06..9323fdd 100644 --- a/lib/ui_kit/widgets/room/room_msg_item.dart +++ b/lib/ui_kit/widgets/room/room_msg_item.dart @@ -711,13 +711,7 @@ class _MsgItemState extends State { ), ); spans.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Image.asset( - "sc_images/room/sc_icon_room_rocket_lv${widget.msg.number}.png", - width: 18.w, - ), - ), + WidgetSpan(alignment: PlaceholderAlignment.middle, child: _rocketIcon()), ); spans.add( TextSpan( @@ -762,6 +756,14 @@ class _MsgItemState extends State { ); } + Widget _rocketIcon() { + final iconUrl = widget.msg.rocketIconUrl?.trim() ?? ''; + if (iconUrl.isEmpty) { + return SizedBox(width: 4.w); + } + return netImage(url: iconUrl, width: 18.w); + } + ///幸运礼物 _buildGameLuckyGiftMsg(BuildContext context) { List spans = []; @@ -1875,6 +1877,7 @@ class Msg { String? type; SocialChatUserProfile? user; String? role = ""; + String? rocketIconUrl = ""; SocialChatGiftRes? gift; //礼物个数 @@ -1904,6 +1907,7 @@ class Msg { this.toUser, this.userWheat, this.role = "", + this.rocketIconUrl, this.gift, this.number, this.customAnimationCount, @@ -1921,6 +1925,7 @@ class Msg { msg = json['msg']; type = json['type']; role = json['role']; + rocketIconUrl = json['rocketIconUrl']?.toString(); time = json['time']; number = json['number']; customAnimationCount = json['customAnimationCount']; @@ -1957,6 +1962,9 @@ class Msg { map['msg'] = msg; map['type'] = type; map['role'] = role; + if ((rocketIconUrl ?? "").trim().isNotEmpty) { + map['rocketIconUrl'] = rocketIconUrl; + } map['time'] = time; map['awardAmount'] = awardAmount; map['number'] = number; diff --git a/local_packages/pag-1.0.7-patched/CHANGELOG.md b/local_packages/pag-1.0.7-patched/CHANGELOG.md new file mode 100644 index 0000000..905702f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/CHANGELOG.md @@ -0,0 +1,30 @@ +## 1.0.7 +* 修复1.0.6安卓代码未正确包含的问题 + +## 1.0.6 +* 提升libpag库版本 +* 安卓库提升kotlin-gradle-plugin版本 +* 安卓原生侧添加缓存,加快加载速度 +* 安卓原生侧支持多线程,优化卡顿情况 +* !!!未正确发布,不可用!!! + +## 1.0.5 +* 增加鸿蒙支持 + +## 1.0.4 +* 增加展位图参数 + +## 1.0.3 +* 删除gradle无用引用 + +## 1.0.2 +* 修正Android repeatCount空异常 + +## 1.0.1 +* 修正命名规范的疏漏,podspec未修改 + +## 1.0.0 +* 封装PAG动画使用方式 +* 支持二进制、网络连接、资源文件等加载方式 +* 提供动画流程中的事件监听,与原生API一致 + diff --git a/local_packages/pag-1.0.7-patched/LICENSE b/local_packages/pag-1.0.7-patched/LICENSE new file mode 100644 index 0000000..5eae826 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/LICENSE @@ -0,0 +1,1350 @@ +Tencent is pleased to support the open source community by making libpag available. + +Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. The below software in this distribution may have been modified by THL A29 Limited ("Tencent Modifications"). All Tencent Modifications are Copyright (C) THL A29 Limited. + +libpag is licensed under the Apache License Version 2.0 except for the third-party components listed below. + + +Terms of the Apache License Version 2.0: +-------------------------------------------------------------------- +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + + +Other dependencies and licenses: + + +Open Source Software Licensed under the Apache License Version 2.0: +The below software in this distribution may have been modified by THL A29 Limited ("Tencent Modifications"). All Tencent Modifications are Copyright (C) 2021 THL A29 Limited. +-------------------------------------------------------------------- +1. libavc +Copyright (C) 2015 The Android Open Source Project +Please note this software has been modified by Tencent in this distribution. + + +Terms of the Apache License Version 2.0: +-------------------------------------------------------------------- +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + + +Open Source Software Licensed under the BSD 3-Clause License: +The below software in this distribution may have been modified by Tencent. +-------------------------------------------------------------------- +1. libwebp +Copyright (c) 2010, Google Inc. All rights reserved. + +2. pathKit +Copyright (c) 2011 Google Inc. All rights reserved. + +3. skcms +Copyright 2018 Google Inc. + + +Terms of the BSD 3-Clause License: +-------------------------------------------------------------------- +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +Open Source Software Licensed under the FreeType License and Other Licenses of the Third-Party Components therein: +-------------------------------------------------------------------- +1. freetype +Copyright (C) 2006-2022 by +David Turner, Robert Wilhelm, and Werner Lemberg. + +FREETYPE LICENSES +----------------- +The FreeType 2 font engine is copyrighted work and cannot be used +legally without a software license. In order to make this project +usable to a vast majority of developers, we distribute it under two +mutually exclusive open-source licenses. +This means that *you* must choose *one* of the two licenses described +below, then obey all its terms and conditions when using FreeType 2 in +any of your projects or products. + - The FreeType License, found in the file `docs/FTL.TXT`, which is + similar to the original BSD license *with* an advertising clause + that forces you to explicitly cite the FreeType project in your + product's documentation. All details are in the license file. + This license is suited to products which don't use the GNU General + Public License. + Note that this license is compatible to the GNU General Public + License version 3, but not version 2. + - The GNU General Public License version 2, found in + `docs/GPLv2.TXT` (any later version can be used also), for + programs which already use the GPL. Note that the FTL is + incompatible with GPLv2 due to its advertisement clause. +The contributed BDF and PCF drivers come with a license similar to +that of the X Window System. It is compatible to the above two +licenses (see files `src/bdf/README` and `src/pcf/README`). The same +holds for the source code files `src/base/fthash.c` and +`include/freetype/internal/fthash.h`; they wer part of the BDF driver +in earlier FreeType versions. +The gzip module uses the zlib license (see `src/gzip/zlib.h`) which +too is compatible to the above two licenses. +The MD5 checksum support (only used for debugging in development +builds) is in the public domain. +--- end of LICENSE.TXT --- + + The FreeType Project LICENSE + ---------------------------- + 2006-Jan-27 + Copyright 1996-2002, 2006 by + David Turner, Robert Wilhelm, and Werner Lemberg +Introduction +============ + The FreeType Project is distributed in several archive packages; + some of them may contain, in addition to the FreeType font engine, + various tools and contributions which rely on, or relate to, the + FreeType Project. + This license applies to all files found in such packages, and + which do not fall under their own explicit license. The license + affects thus the FreeType font engine, the test programs, + documentation and makefiles, at the very least. + This license was inspired by the BSD, Artistic, and IJG + (Independent JPEG Group) licenses, which all encourage inclusion + and use of free software in commercial and freeware products + alike. As a consequence, its main points are that: + o We don't promise that this software works. However, we will be + interested in any kind of bug reports. (`as is' distribution) + o You can use this software for whatever you want, in parts or + full form, without having to pay us. (`royalty-free' usage) + o You may not pretend that you wrote this software. If you use + it, or only parts of it, in a program, you must acknowledge + somewhere in your documentation that you have used the + FreeType code. (`credits') + We specifically permit and encourage the inclusion of this + software, with or without modifications, in commercial products. + We disclaim all warranties covering The FreeType Project and + assume no liability related to The FreeType Project. + Finally, many people asked us for a preferred form for a + credit/disclaimer to use in compliance with this license. We thus + encourage you to use the following text: + """ + Portions of this software are copyright © The FreeType + Project (www.freetype.org). All rights reserved. + """ + Please replace with the value from the FreeType version you + actually use. +Legal Terms +=========== +0. Definitions +-------------- + Throughout this license, the terms `package', `FreeType Project', + and `FreeType archive' refer to the set of files originally + distributed by the authors (David Turner, Robert Wilhelm, and + Werner Lemberg) as the `FreeType Project', be they named as alpha, + beta or final release. + `You' refers to the licensee, or person using the project, where + `using' is a generic term including compiling the project's source + code as well as linking it to form a `program' or `executable'. + This program is referred to as `a program using the FreeType + engine'. + This license applies to all files distributed in the original + FreeType Project, including all source code, binaries and + documentation, unless otherwise stated in the file in its + original, unmodified form as distributed in the original archive. + If you are unsure whether or not a particular file is covered by + this license, you must contact us to verify this. + The FreeType Project is copyright (C) 1996-2000 by David Turner, + Robert Wilhelm, and Werner Lemberg. All rights reserved except as + specified below. +1. No Warranty +-------------- + THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO + USE, OF THE FREETYPE PROJECT. +2. Redistribution +----------------- + This license grants a worldwide, royalty-free, perpetual and + irrevocable right and license to use, execute, perform, compile, + display, copy, create derivative works of, distribute and + sublicense the FreeType Project (in both source and object code + forms) and derivative works thereof for any purpose; and to + authorize others to exercise some or all of the rights granted + herein, subject to the following conditions: + o Redistribution of source code must retain this license file + (`FTL.TXT') unaltered; any additions, deletions or changes to + the original files must be clearly indicated in accompanying + documentation. The copyright notices of the unaltered, + original files must be preserved in all copies of source + files. + o Redistribution in binary form must provide a disclaimer that + states that the software is based in part of the work of the + FreeType Team, in the distribution documentation. We also + encourage you to put an URL to the FreeType web page in your + documentation, though this isn't mandatory. + These conditions apply to any software derived from or based on + the FreeType Project, not just the unmodified files. If you use + our work, you must acknowledge us. However, no fee need be paid + to us. +3. Advertising +-------------- + Neither the FreeType authors and contributors nor you shall use + the name of the other for commercial, advertising, or promotional + purposes without specific prior written permission. + We suggest, but do not require, that you use one or more of the + following phrases to refer to this software in your documentation + or advertising materials: `FreeType Project', `FreeType Engine', + `FreeType library', or `FreeType Distribution'. + As you have not signed this license, you are not required to + accept it. However, as the FreeType Project is copyrighted + material, only this license, or another one contracted with the + authors, grants you the right to use, distribute, and modify it. + Therefore, by using, distributing, or modifying the FreeType + Project, you indicate that you understand and accept all the terms + of this license. +4. Contacts +----------- + There are two mailing lists related to FreeType: + o freetype@nongnu.org + Discusses general use and applications of FreeType, as well as + future and wanted additions to the library and distribution. + If you are looking for support, start in this list if you + haven't found anything to help you in the documentation. + o freetype-devel@nongnu.org + Discusses bugs, as well as engine internals, design issues, + specific licenses, porting, etc. + Our home page can be found at + https://www.freetype.org +--- end of FTL.TXT --- + + + +Open Source Software Licensed under the IJG License, the BSD 3-Clause License and the Zlib License: +-------------------------------------------------------------------- +1. libjpeg-turbo +Copyright (c) libjpeg-turbo original author and authors + +libjpeg-turbo Licenses +====================== + +libjpeg-turbo is covered by three compatible BSD-style open source licenses: + +- The IJG (Independent JPEG Group) License, which is listed in + [README.ijg](README.ijg) + + This license applies to the libjpeg API library and associated programs + (any code inherited from libjpeg, and any modifications to that code.) + +- The Modified (3-clause) BSD License, which is listed below + + This license covers the TurboJPEG API library and associated programs, as + well as the build system. + +- The zlib License, which is listed below + + This license is a subset of the other two, and it covers the libjpeg-turbo + SIMD extensions. + + +Complying with the libjpeg-turbo Licenses +========================================= + +This section provides a roll-up of the libjpeg-turbo licensing terms, to the +best of our understanding. + +1. If you are distributing a modified version of the libjpeg-turbo source, + then: + + 1. You cannot alter or remove any existing copyright or license notices + from the source. + + **Origin** + - Clause 1 of the IJG License + - Clause 1 of the Modified BSD License + - Clauses 1 and 3 of the zlib License + + 2. You must add your own copyright notice to the header of each source + file you modified, so others can tell that you modified that file (if + there is not an existing copyright header in that file, then you can + simply add a notice stating that you modified the file.) + + **Origin** + - Clause 1 of the IJG License + - Clause 2 of the zlib License + + 3. You must include the IJG README file, and you must not alter any of the + copyright or license text in that file. + + **Origin** + - Clause 1 of the IJG License + +2. If you are distributing only libjpeg-turbo binaries without the source, or + if you are distributing an application that statically links with + libjpeg-turbo, then: + + 1. Your product documentation must include a message stating: + + This software is based in part on the work of the Independent JPEG + Group. + + **Origin** + - Clause 2 of the IJG license + + 2. If your binary distribution includes or uses the TurboJPEG API, then + your product documentation must include the text of the Modified BSD + License. + + **Origin** + - Clause 2 of the Modified BSD License + +3. You cannot use the name of the IJG or The libjpeg-turbo Project or the + contributors thereof in advertising, publicity, etc. + + **Origin** + - IJG License + - Clause 3 of the Modified BSD License + +4. The IJG and The libjpeg-turbo Project do not warrant libjpeg-turbo to be + free of defects, nor do we accept any liability for undesirable + consequences resulting from your use of the software. + + **Origin** + - IJG License + - Modified BSD License + - zlib License + + +The Modified (3-clause) BSD License +=================================== + +Copyright (C)\ \. All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of the libjpeg-turbo Project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +The zlib License +================ + +Copyright (C) \, \. + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +Why Three Licenses? +=================== + +The zlib License could have been used instead of the Modified (3-clause) BSD +License, and since the IJG License effectively subsumes the distribution +conditions of the zlib License, this would have effectively placed +libjpeg-turbo binary distributions under the IJG License. However, the IJG +License specifically refers to the Independent JPEG Group and does not extend +attribution and endorsement protections to other entities. Thus, it was +desirable to choose a license that granted us the same protections for new code +that were granted to the IJG for code derived from their software. + + +LEGAL ISSUES +============ + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2016, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent (now expired), GIF reading +support has been removed altogether, and the GIF writer has been simplified +to produce "uncompressed GIFs". This technique does not use the LZW +algorithm; the resulting GIF files are larger than usual, but are readable +by all standard GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + + + +Open Source Software Licensed under the libpng license: +-------------------------------------------------------------------- +1. libpng +Copyright (c) 2000-2002, 2004, 2006-2017 Glenn Randers-Pehrson + + +Terms of the libpng license: +-------------------------------------------------------------------- +This copy of the libpng notices is provided for your convenience. In case of +any discrepancy between this copy and the notices in the file png.h that is +included in the libpng distribution, the latter shall prevail. + +COPYRIGHT NOTICE, DISCLAIMER, and LICENSE: + +If you modify libpng you may insert additional notices immediately following +this sentence. + +This code is released under the libpng license. + +libpng versions 1.0.7, July 1, 2000 through 1.6.33, September 28, 2017 are +Copyright (c) 2000-2002, 2004, 2006-2017 Glenn Randers-Pehrson, are +derived from libpng-1.0.6, and are distributed according to the same +disclaimer and license as libpng-1.0.6 with the following individuals +added to the list of Contributing Authors: + + Simon-Pierre Cadieux + Eric S. Raymond + Mans Rullgard + Cosmin Truta + Gilles Vollant + James Yu + Mandar Sahastrabuddhe + Google Inc. + Vadim Barkov + +and with the following additions to the disclaimer: + + There is no warranty against interference with your enjoyment of the + library or against infringement. There is no warranty that our + efforts or the library will fulfill any of your particular purposes + or needs. This library is provided with all faults, and the entire + risk of satisfactory quality, performance, accuracy, and effort is with + the user. + +Some files in the "contrib" directory and some configure-generated +files that are distributed with libpng have other copyright owners and +are released under other open source licenses. + +libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are +Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from +libpng-0.96, and are distributed according to the same disclaimer and +license as libpng-0.96, with the following individuals added to the list +of Contributing Authors: + + Tom Lane + Glenn Randers-Pehrson + Willem van Schaik + +libpng versions 0.89, June 1996, through 0.96, May 1997, are +Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, +and are distributed according to the same disclaimer and license as +libpng-0.88, with the following individuals added to the list of +Contributing Authors: + + John Bowler + Kevin Bracey + Sam Bushell + Magnus Holmgren + Greg Roelofs + Tom Tanner + +Some files in the "scripts" directory have other copyright owners +but are released under this license. + +libpng versions 0.5, May 1995, through 0.88, January 1996, are +Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +For the purposes of this copyright and license, "Contributing Authors" +is defined as the following set of individuals: + + Andreas Dilger + Dave Martindale + Guy Eric Schalnat + Paul Schmidt + Tim Wegner + +The PNG Reference Library is supplied "AS IS". The Contributing Authors +and Group 42, Inc. disclaim all warranties, expressed or implied, +including, without limitation, the warranties of merchantability and of +fitness for any purpose. The Contributing Authors and Group 42, Inc. +assume no liability for direct, indirect, incidental, special, exemplary, +or consequential damages, which may result from the use of the PNG +Reference Library, even if advised of the possibility of such damage. + +Permission is hereby granted to use, copy, modify, and distribute this +source code, or portions hereof, for any purpose, without fee, subject +to the following restrictions: + + 1. The origin of this source code must not be misrepresented. + + 2. Altered versions must be plainly marked as such and must not + be misrepresented as being the original source. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + +The Contributing Authors and Group 42, Inc. specifically permit, without +fee, and encourage the use of this source code as a component to +supporting the PNG file format in commercial products. If you use this +source code in a product, acknowledgment is not required but would be +appreciated. + +END OF COPYRIGHT NOTICE, DISCLAIMER, and LICENSE. + +TRADEMARK: + +The name "libpng" has not been registered by the Copyright owner +as a trademark in any jurisdiction. However, because libpng has +been distributed and maintained world-wide, continually since 1995, +the Copyright owner claims "common-law trademark protection" in any +jurisdiction where common-law trademark is recognized. + +OSI CERTIFICATION: + +Libpng is OSI Certified Open Source Software. OSI Certified Open Source is +a certification mark of the Open Source Initiative. OSI has not addressed +the additional disclaimers inserted at version 1.0.7. + +EXPORT CONTROL: + +The Copyright owner believes that the Export Control Classification +Number (ECCN) for libpng is EAR99, which means not subject to export +controls or International Traffic in Arms Regulations (ITAR) because +it is open source, publicly available software, that does not contain +any encryption software. See the EAR, paragraphs 734.3(b)(3) and +734.7(b). + +Glenn Randers-Pehrson +glennrp at users.sourceforge.net +September 28, 2017 + + + +Open Source Software Licensed under the MIT License and Other Licenses of the Third-Party Components therein: +-------------------------------------------------------------------- +1. rttr +Copyright (c) 2014 - 2018 Axel Menzel + + +Terms of the MIT License: +-------------------------------------------------------------------- +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +For third party catch: + + * Catch v1.12.0 + * Generated: 2018-01-11 21:56:34.893972 + * ---------------------------------------------------------- + * This file has been merged from multiple headers. Please don't edit it directly + * Copyright (c) 2012 Two Blue Cubes Ltd. All rights reserved. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +For third party nonius: + +// Nonius - C++ benchmarking tool +// +// Written in 2014 by Martinho Fernandes +// +// To the extent possible under law, the author(s) have dedicated all copyright and related +// and neighboring rights to this software to the public domain worldwide. This software is +// distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along with this software. +// If not, see + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. +Statement of Purpose +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: + +the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; +moral rights retained by the original author(s) and/or performer(s); +publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; +rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; +rights protecting the extraction, dissemination, use and reuse of data in a Work; +database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and +other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + +No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. +Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. +Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. +Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + + +For third party rapidjson: + +// Tencent is pleased to support the open source community by making RapidJSON available. +// +// Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. +// +// Licensed under the MIT License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + + + +Open Source Software Licensed under the Zlib License: +-------------------------------------------------------------------- +1. zlib +(C) 1995-2017 Jean-loup Gailly and Mark Adler + + +Terms of the Zlib License: +-------------------------------------------------------------------- + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + The data format used by the zlib library is described by RFCs (Request for + Comments) 1950 to 1952 in the files http://tools.ietf.org/html/rfc1950 + (zlib format), rfc1951 (deflate format) and rfc1952 (gzip format). + + + +Open Source Software Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 2.1: +-------------------------------------------------------------------- +1. ffavc +Copyright (c) 2021 Tencent. All rights reserved. + + +Terms of the GNU LESSER GENERAL PUBLIC LICENSE Version 2.1: +-------------------------------------------------------------------- + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice +That's all there is to it! diff --git a/local_packages/pag-1.0.7-patched/README.md b/local_packages/pag-1.0.7-patched/README.md new file mode 100644 index 0000000..a4968ac --- /dev/null +++ b/local_packages/pag-1.0.7-patched/README.md @@ -0,0 +1,92 @@ +# 项目介绍 +为Flutter打造的PAG动画组件,以外接纹理的方式实现。 + +**注:如果遇到使用问题请在本仓库提 issue 与作者讨论,或直接提交 pr 参与共建。** + +[**PAG官网**](https://pag.art/) + +# 快速上手 +Flutter侧通过PAGView来使用动画 + +### 引用 +``` +dependencies: + pag: 1.0.0 +``` + +Android端混淆文件中配置,避免影响 +``` +-keep class org.libpag.**{*;} +``` + +### 使用本地资源 +``` +PAGView.asset( + "assets/xxx.pag", //flutter侧资源路径 + repeatCount: PagView.REPEAT_COUNT_LOOP, // 循环次数 + initProgress: 0.25, // 初始进度 + key: pagKey, // 利用key进行主动调用 + autoPlay: true, // 是否自动播放 + ) +``` +### 使用网络资源 +``` +PAGView.url( + "xxxx", //网络链接 + repeatCount: PagView.REPEAT_COUNT_LOOP, // 循环次数 + initProgress: 0.25, // 初始进度 + key: pagKey, // 利用key进行主动调用 + autoPlay: true, // 是否自动播放 + ) +``` +### 使用二进制数据 +``` +PAGView.bytes( + "xxxx", //网络链接 + repeatCount: PagView.REPEAT_COUNT_LOOP, // 循环次数 + initProgress: 0.25, // 初始进度 + key: pagKey, // 利用key进行主动调用 + autoPlay: true, // 是否自动播放 + ) +``` +### 可以在PAGView中加入回调参数 +以下回调与原生PAG监听对齐 +``` +PAGView.asset( + ... + onAnimationStart: (){ // 开始 + // do something + }, + onAnimationEnd: (){ // 结束 + // do something + }, + onAnimationRepeat: (){ // 重复 + // do something + }, + onAnimationCancel: (){ // 取消 + // do something + }, +``` + +### 通过key获取state进行主动调用 +``` + final GlobalKey pagKey = GlobalKey(); + + //传入key值 + PAGView.url(key:pagKey) + + //播放 + pagKey.currentState?.start(); + + //暂停 + pagKey.currentState?.pause(); + + //停止 + pagKey.currentState?.stop(); + + //设置进度 + pagKey.currentState?.setProgress(xxx); + + //获取坐标位置的图层名list + pagKey.currentState?.getLayersUnderPoint(x,y); +``` diff --git a/local_packages/pag-1.0.7-patched/android/build.gradle b/local_packages/pag-1.0.7-patched/android/build.gradle new file mode 100644 index 0000000..bcb266f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/build.gradle @@ -0,0 +1,47 @@ +group 'com.example.flutter_pag_plugin' +version '1.0' + +buildscript { + repositories { + google() + jcenter() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace 'com.example.flutter_pag_plugin' + compileSdkVersion 36 + + defaultConfig { + minSdkVersion 19 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + + +dependencies { + implementation "com.tencent.tav:libpag:4.3.68" + implementation 'com.jakewharton:disklrucache:2.0.2' +} diff --git a/local_packages/pag-1.0.7-patched/android/gradle.properties b/local_packages/pag-1.0.7-patched/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/local_packages/pag-1.0.7-patched/android/gradle/wrapper/gradle-wrapper.properties b/local_packages/pag-1.0.7-patched/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c9d085 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/local_packages/pag-1.0.7-patched/android/settings.gradle b/local_packages/pag-1.0.7-patched/android/settings.gradle new file mode 100644 index 0000000..f1753f0 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_pag_plugin' diff --git a/local_packages/pag-1.0.7-patched/android/src/main/AndroidManifest.xml b/local_packages/pag-1.0.7-patched/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f39af02 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/DataLoadHelper.kt b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/DataLoadHelper.kt new file mode 100644 index 0000000..ebce0ce --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/DataLoadHelper.kt @@ -0,0 +1,210 @@ +package com.example.flutter_pag_plugin + +import android.content.Context +import android.os.Environment +import android.util.Log +import android.util.LruCache +import com.example.flutter_pag_plugin.utils.EncodeUtil +import com.jakewharton.disklrucache.DiskLruCache +import java.io.* +import java.net.HttpURLConnection +import java.net.URL +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +//数据加载器 +object DataLoadHelper { + private var diskCache: DiskLruCache? = null + private val memoryCache by lazy { LruCache(Runtime.getRuntime().maxMemory().toInt() / 50) } + private const val TAG = "DataLoadHelper" + const val DEFAULT_DIS_SIZE = 30 * 1024 * 1024L; + + // 下载来源:flutter插件、其他 + const val FROM_PLUGIN = 0 + const val FROM_OTHER = 1 + + private val loadListeners = mutableListOf() + + //初始化pag动画 + fun loadPag(src: String, addPag: (ByteArray?) -> Unit, from: Int = FROM_OTHER) { + val time = System.currentTimeMillis(); + val bytes = memoryCache.get(hashKeyForDisk(src)) + + loadListeners.forEach { + it.loadStart(src, from) + } + + if (bytes != null) { + addPag(bytes) + loadListeners.forEach { + it.loadComplete(src, bytes, System.currentTimeMillis() - time, "", from) + } + } else { + Thread { + loadPagByDisk(src) { byteArray, errorMsg -> + addPag.invoke(byteArray) + loadListeners.forEach { + it.loadComplete(src, byteArray, System.currentTimeMillis() - time, errorMsg, from) + } + } + }.start() + } + } + + fun initDiskCache(context: Context, size: Long = DEFAULT_DIS_SIZE) { + if (diskCache != null) { + Log.w(TAG, "diskCache do not need init again!") + return + } + + val cacheDir = getDiskCacheDir(context, "pag") + if (!cacheDir.exists()) { + cacheDir.mkdirs() + } + try { + diskCache = diskCache ?: DiskLruCache.open( + cacheDir, + context.packageManager.getPackageInfo(context.packageName, 0).versionCode, + 1, size + ) + } catch (e: IOException) { + Log.e(TAG, "initDiskCache error: $e") + } + } + + + //获取硬盘缓存路径 + private fun getDiskCacheDir(context: Context, uniqueName: String): File { + val cachePath: String = if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() + || !Environment.isExternalStorageRemovable() + ) { + context.externalCacheDir?.path + } else { + context.cacheDir.path + } ?: "" + return File(cachePath + File.separator.toString() + uniqueName) + } + + + //硬盘或者网络获取, + @Synchronized + private fun loadPagByDisk(src: String, addPag: (ByteArray?, String) -> Unit) { + //从硬盘缓存中获取 + + var errorMsg = "" + val key = hashKeyForDisk(src) + var snapShot: DiskLruCache.Snapshot? = null + try { + snapShot = diskCache?.get(key) + if (snapShot == null) { + Log.d(TAG, "loadPag load from network") + //没有,进行网络操作 + val editor = diskCache?.edit(key) + if (editor != null) { + val outputStream = editor.newOutputStream(0) + if (downloadUrlToStream(src, outputStream)) { + editor.commit() + } else { + editor.abort() + } + } + //存入硬盘后再次读取 + diskCache?.flush() + snapShot = diskCache?.get(key) + } + } catch (e: IOException) { + Log.e(TAG, "loadPag load from network erro: $e") + errorMsg = "loadPag load from network error: $e" + } + + //进行bytes读取 + var bytes: ByteArray? = null + if (snapShot != null) { + Log.d(TAG, "loadPag load from snapShot") + try { + val inputStream = snapShot.getInputStream(0) + val outputStrem = ByteArrayOutputStream() + inputStream.use { ins -> + outputStrem.use { bos -> + val buffer = ByteArray(1024) + var len: Int + + while (ins.read(buffer).also { len = it } != -1) { + bos.write(buffer, 0, len) + } + bytes = bos.toByteArray() + } + } + } catch (e: IOException) { + e.printStackTrace() + Log.e(TAG, "loadPag load from snapShot erro: $e") + errorMsg = "loadPag load from network error: $e" + } + } + + Log.d(TAG, "loadPag bytes size: ${bytes?.size}") + //存储进内存缓存 + if (bytes != null && bytes!!.isNotEmpty() && memoryCache.get(key) == null) { + memoryCache.put(key, bytes) + } + addPag(bytes, errorMsg) + } + + + //下载 + private fun downloadUrlToStream(urlString: String, outputStream: OutputStream): Boolean { + var urlConnection: HttpURLConnection? = null + var outStream: BufferedOutputStream? = null + var inStream: BufferedInputStream? = null + try { + val url = URL(urlString) + urlConnection = url.openConnection() as HttpURLConnection + inStream = BufferedInputStream(urlConnection.inputStream, 8 * 1024) + outStream = BufferedOutputStream(outputStream, 8 * 1024) + var b: Int + while (inStream.read().also { b = it } != -1) { + outStream.write(b) + } + return true + } catch (e: IOException) { + e.printStackTrace() + } finally { + urlConnection?.disconnect() + try { + outStream?.close() + inStream?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + return false + } + + //使用MD5算法对传入的key进行加密并返回 + private fun hashKeyForDisk(key: String): String? { + return try { + val mDigest: MessageDigest = MessageDigest.getInstance("MD5") + mDigest.update(key.toByteArray()) + EncodeUtil.bytesToHexString(mDigest.digest()) + } catch (e: NoSuchAlgorithmException) { + key.hashCode().toString() + } + } + + fun addLoadListener(loadListener: ILoadListener) { + loadListeners.add(loadListener) + } + + + fun removeLoadListener(loadListener: ILoadListener) { + loadListeners.remove(loadListener) + } +} + +// 用于监听PAG加载情况 +interface ILoadListener { + + fun loadStart(url: String, from: Int) + + fun loadComplete(url: String, result: ByteArray?/*result为空则失败*/, useTime: Long, errorMsg: String, from: Int) +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/FlutterPagPlayer.java b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/FlutterPagPlayer.java new file mode 100644 index 0000000..2b37d5f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/FlutterPagPlayer.java @@ -0,0 +1,230 @@ +package com.example.flutter_pag_plugin; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.graphics.SurfaceTexture; +import android.view.animation.LinearInterpolator; + +import org.libpag.PAGFile; +import org.libpag.PAGPlayer; +import org.libpag.PAGSurface; + +import java.util.HashMap; + +import io.flutter.plugin.common.MethodChannel; + + +public class FlutterPagPlayer extends PAGPlayer { + + private final ValueAnimator animator = ValueAnimator.ofFloat(0.0F, 1.0F); + private boolean isRelease; + private long currentPlayTime = 0L; + private double progress = 0; + private double initProgress = 0; + private SurfaceTexture surfaceTexture; + + private MethodChannel channel; + private long textureId; + + + public FlutterPagPlayer() { + super(); + animator.setInterpolator(new LinearInterpolator()); + animator.addUpdateListener(animatorUpdateListener); + animator.addListener(animatorListenerAdapter); + } + + public boolean isRelease() { + return isRelease; + } + + public void init(PAGFile file, int repeatCount, double initProgress, MethodChannel channel, long textureId) { + if (WorkThreadExecutor.multiThread) { + synchronized (this) { + setComposition(file); + } + } else { + setComposition(file); + } + + this.channel = channel; + this.textureId = textureId; + progress = initProgress; + this.initProgress = initProgress; + initAnimator(repeatCount); + } + + private boolean valid() { + return getSurface() != null && surfaceTexture != null; + } + + private void initAnimator(int repeatCount) { + animator.setDuration(duration() / 1000L); + if (repeatCount < 0) { + repeatCount = 0; + } + animator.setRepeatCount(repeatCount - 1); + setProgressValue(initProgress); + } + + public void setProgressValue(double value) { + this.progress = Math.max(0.0D, Math.min(value, 1.0D)); + this.currentPlayTime = (long) (progress * (double) this.animator.getDuration()); + this.animator.setCurrentPlayTime(currentPlayTime); + setProgress(progress); + flush(); + } + + public void start() { + animator.start(); + } + + public void stop() { + pause(); + setProgressValue(initProgress); + } + + @Override + public void setSurface(PAGSurface pagSurface) { + super.setSurface(pagSurface); + } + + public void setSurfaceTexture(SurfaceTexture surfaceTexture) { + this.surfaceTexture = surfaceTexture; + } + + public void updateBufferSize(int width, int height) { + if (WorkThreadExecutor.multiThread) { + synchronized (this) { + surfaceTexture.setDefaultBufferSize(width, height); + getSurface().updateSize(); + getSurface().clearAll(); + } + } else { + surfaceTexture.setDefaultBufferSize(width, height); + getSurface().updateSize(); + getSurface().clearAll(); + } + } + + public void clear() { + if (WorkThreadExecutor.multiThread) { + synchronized (this) { + setComposition(null); + if (valid()) { + getSurface().freeCache(); + getSurface().clearAll(); + } + } + } else { + setComposition(null); + if (valid()) { + getSurface().freeCache(); + getSurface().clearAll(); + } + } + } + + public void cancel() { + animator.cancel(); + } + + public void pause() { + animator.pause(); + } + + @Override + public void release() { + super.release(); + animator.cancel(); + animator.removeAllUpdateListeners(); + animator.removeAllListeners(); + WorkThreadExecutor.getInstance().post(() -> { + if (WorkThreadExecutor.multiThread) { + synchronized (this) { + if (getSurface() != null) getSurface().release(); + surfaceTexture.release(); + surfaceTexture = null; + } + } else { + if (getSurface() != null) getSurface().release(); + surfaceTexture.release(); + surfaceTexture = null; + } + + }); + isRelease = true; + } + + @Override + public boolean flush() { + if (isRelease) { + return false; + } + WorkThreadExecutor.getInstance().post(() -> { + if (WorkThreadExecutor.multiThread) { + synchronized (this) { + FlutterPagPlayer.super.flush(); + } + } else { + FlutterPagPlayer.super.flush(); + } + + }); + return true; + +// return super.flush(); + } + + // 更新PAG渲染 + private final ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + progress = (double) (Float) animation.getAnimatedValue(); + currentPlayTime = (long) (progress * (double) animator.getDuration()); + setProgress(progress); + flush(); + } + }; + + // 动画状态监听 + private final AnimatorListenerAdapter animatorListenerAdapter = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + super.onAnimationStart(animator); + notifyEvent(FlutterPagPlugin._eventStart); + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + // Align with iOS platform, avoid triggering this method when stopping + int repeatCount = ((ValueAnimator) animation).getRepeatCount(); + if (repeatCount >= 0 && (animation.getDuration() > 0) && + (currentPlayTime / animation.getDuration() > repeatCount)) { + notifyEvent(FlutterPagPlugin._eventEnd); + } + } + + @Override + public void onAnimationCancel(Animator animator) { + super.onAnimationCancel(animator); + notifyEvent(FlutterPagPlugin._eventCancel); + } + + @Override + public void onAnimationRepeat(Animator animator) { + super.onAnimationRepeat(animator); + notifyEvent(FlutterPagPlugin._eventRepeat); + } + }; + + void notifyEvent(String event) { + final HashMap arguments = new HashMap<>(); + arguments.put(FlutterPagPlugin._argumentTextureId, textureId); + arguments.put(FlutterPagPlugin._argumentEvent, event); + channel.invokeMethod(FlutterPagPlugin._playCallback, arguments); + } +} diff --git a/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/FlutterPagPlugin.java b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/FlutterPagPlugin.java new file mode 100644 index 0000000..52eb3da --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/FlutterPagPlugin.java @@ -0,0 +1,387 @@ +package com.example.flutter_pag_plugin; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.Surface; + +import androidx.annotation.NonNull; + +import org.libpag.PAGFile; +import org.libpag.PAGLayer; +import org.libpag.PAGSurface; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.view.TextureRegistry; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; + +/** + * FlutterPagPlugin + */ +public class FlutterPagPlugin implements FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private MethodChannel channel; + TextureRegistry textureRegistry; + Context context; + FlutterPlugin.FlutterAssets flutterAssets; + private Handler handler = new Handler(Looper.getMainLooper()); + + // 多引擎使用是plugin的集合,请留意该场景下需手动释放,否则存在内存泄漏的问题 + public static List pluginList = new ArrayList(); + + public HashMap layerMap = new HashMap(); + public HashMap entryMap = new HashMap(); + public LinkedList freeEntryPool = new LinkedList<>(); + + // 原生接口 + final static String _nativeInit = "initPag"; + final static String _nativeRelease = "release"; + final static String _nativeStart = "start"; + final static String _nativeStop = "stop"; + final static String _nativePause = "pause"; + final static String _nativeSetProgress = "setProgress"; + final static String _nativeGetPointLayer = "getLayersUnderPoint"; + final static String _nativeEnableCache = "enableCache"; + final static String _nativeSetCacheSize = "setCacheSize"; + final static String _nativeEnableMultiThread = "enableMultiThread"; + + + // 参数 + final static String _argumentTextureId = "textureId"; + final static String _argumentAssetName = "assetName"; + final static String _argumentPackage = "package"; + final static String _argumentUrl = "url"; + final static String _argumentBytes = "bytesData"; + final static String _argumentRepeatCount = "repeatCount"; + final static String _argumentInitProgress = "initProgress"; + final static String _argumentAutoPlay = "autoPlay"; + final static String _argumentWidth = "width"; + final static String _argumentHeight = "height"; + final static String _argumentPointX = "x"; + final static String _argumentPointY = "y"; + final static String _argumentProgress = "progress"; + final static String _argumentEvent = "PAGEvent"; + final static String _argumentCacheEnabled = "cacheEnabled"; + final static String _argumentCacheSize = "cacheSize"; + final static String _argumentMultiThreadEnabled = "multiThreadEnabled"; + + // 回调 + final static String _playCallback = "PAGCallback"; + final static String _eventStart = "onAnimationStart"; + final static String _eventEnd = "onAnimationEnd"; + final static String _eventCancel = "onAnimationCancel"; + final static String _eventRepeat = "onAnimationRepeat"; + final static String _eventUpdate = "onAnimationUpdate"; + + private boolean useCache = true; + private int maxFreePoolSize = 10; + + public FlutterPagPlugin() { + } + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + if (!pluginList.contains(this)) { + pluginList.add(this); + } + flutterAssets = binding.getFlutterAssets(); + channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_pag_plugin"); + channel.setMethodCallHandler(this); + context = binding.getApplicationContext(); + textureRegistry = binding.getTextureRegistry(); + DataLoadHelper.INSTANCE.initDiskCache(context, DataLoadHelper.INSTANCE.DEFAULT_DIS_SIZE); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + switch (call.method) { + case _nativeInit: + initPag(call, result); + break; + case _nativeStart: + start(call); + result.success(""); + break; + case _nativeStop: + stop(call); + result.success(""); + break; + case _nativePause: + pause(call); + result.success(""); + break; + case _nativeSetProgress: + setProgress(call); + result.success(""); + break; + case _nativeRelease: + release(call); + result.success(""); + break; + case _nativeGetPointLayer: + result.success(getLayersUnderPoint(call)); + break; + case _nativeEnableCache: + enableCache(call); + result.success(""); + break; + case _nativeSetCacheSize: + setCacheSize(call); + result.success(""); + break; + case _nativeEnableMultiThread: + enableMultiThread(call); + result.success(""); + break; + default: + result.notImplemented(); + break; + } + } + + private void enableCache(final MethodCall call) { + useCache = call.argument(_argumentCacheEnabled); + } + + private void setCacheSize(final MethodCall call) { + maxFreePoolSize = call.argument(_argumentCacheSize); + } + + private void enableMultiThread(final MethodCall call) { + WorkThreadExecutor.getInstance().enableMultiThread(call.argument(_argumentMultiThreadEnabled)); + } + + + private void initPag(final MethodCall call, final Result result) { + String assetName = call.argument(_argumentAssetName); + byte[] bytes = call.argument(_argumentBytes); + String url = call.argument(_argumentUrl); + String flutterPackage = call.argument(_argumentPackage); + + if (bytes != null) { + initPagPlayerAndCallback(PAGFile.Load(bytes), call, result); + } else if (assetName != null) { + String assetKey; + if (flutterAssets != null) { + if (flutterPackage == null || flutterPackage.isEmpty()) { + assetKey = flutterAssets.getAssetFilePathByName(assetName); + } else { + assetKey = flutterAssets.getAssetFilePathByName(assetName, flutterPackage); + } + } else { + assetKey = ""; + } + + if (assetKey == null) { + result.error("-1100", "asset资源加载错误", null); + return; + } + WorkThreadExecutor.getInstance().post(() -> { + PAGFile composition = PAGFile.Load(context.getAssets(), assetKey); + handler.post(() -> initPagPlayerAndCallback(composition, call, result)); + }); + } else if (url != null) { + DataLoadHelper.INSTANCE.loadPag(url, new Function1() { + @Override + public Unit invoke(final byte[] bytes) { + handler.post(new Runnable() { + @Override + public void run() { + if (bytes == null) { + result.error("-1100", "url资源加载错误", null); + return; + } + + initPagPlayerAndCallback(PAGFile.Load(bytes), call, result); + } + }); + + return null; + } + }, DataLoadHelper.FROM_PLUGIN); + } else { + result.error("-1100", "未添加资源", null); + } + } + + private void initPagPlayerAndCallback(PAGFile composition, MethodCall call, final Result result) { + if (composition == null) { + result.error("-1100", "load composition is null! ", null); + return; + } + + final int repeatCount = call.argument(_argumentRepeatCount); + final double initProgress = call.argument(_argumentInitProgress); + final boolean autoPlay = call.argument(_argumentAutoPlay); + final FlutterPagPlayer pagPlayer; + final String currentId; + if (freeEntryPool.isEmpty() || !useCache) { + pagPlayer = new FlutterPagPlayer(); + final TextureRegistry.SurfaceTextureEntry entry = textureRegistry.createSurfaceTexture(); + currentId = String.valueOf(entry.id()); + entryMap.put(String.valueOf(entry.id()), entry); + SurfaceTexture surfaceTexture = entry.surfaceTexture(); + + final Surface surface = new Surface(surfaceTexture); + final PAGSurface pagSurface = PAGSurface.FromSurface(surface); + pagPlayer.setSurface(pagSurface); + pagPlayer.setSurfaceTexture(surfaceTexture); + layerMap.put(String.valueOf(entry.id()), pagPlayer); + } else { + currentId = freeEntryPool.removeFirst(); + pagPlayer = layerMap.get(currentId); + if (pagPlayer == null) { + result.error("-1101", "id异常,未命中缓存!", null); + return; + } + if (pagPlayer.isRelease()) { + result.error("-1102", "PagPlayer异常!", null); + return; + } + } + + WorkThreadExecutor.getInstance().post(() -> { + pagPlayer.updateBufferSize(composition.width(), composition.height()); + pagPlayer.init(composition, repeatCount, initProgress, channel, Long.parseLong(currentId)); + final HashMap callback = new HashMap(); + callback.put(_argumentTextureId, Long.parseLong(currentId)); + callback.put(_argumentWidth, (double) composition.width()); + callback.put(_argumentHeight, (double) composition.height()); + pagPlayer.flush(); + + handler.post(new Runnable() { + @Override + public void run() { + if (autoPlay) { + pagPlayer.start(); + } + result.success(callback); + } + }); + }); + } + + void start(MethodCall call) { + FlutterPagPlayer flutterPagPlayer = getFlutterPagPlayer(call); + if (flutterPagPlayer != null) { + flutterPagPlayer.start(); + } + } + + void stop(MethodCall call) { + FlutterPagPlayer flutterPagPlayer = getFlutterPagPlayer(call); + if (flutterPagPlayer != null) { + flutterPagPlayer.stop(); + } + } + + void pause(MethodCall call) { + FlutterPagPlayer flutterPagPlayer = getFlutterPagPlayer(call); + if (flutterPagPlayer != null) { + flutterPagPlayer.pause(); + } + } + + void setProgress(MethodCall call) { + double progress = call.argument(_argumentProgress); + FlutterPagPlayer flutterPagPlayer = getFlutterPagPlayer(call); + if (flutterPagPlayer != null) { + flutterPagPlayer.setProgressValue(progress); + } + } + + void release(MethodCall call) { + if (useCache && freeEntryPool.size() < maxFreePoolSize) { + FlutterPagPlayer flutterPagPlayer = layerMap.get(getTextureId(call)); + int id = call.argument(_argumentTextureId); + if (flutterPagPlayer != null) { + flutterPagPlayer.cancel(); + WorkThreadExecutor.getInstance().post(() -> { + flutterPagPlayer.clear(); + if (id >= 0) { + handler.post(() -> { + freeEntryPool.add(id + ""); + }); + } + }); + } + } else { + FlutterPagPlayer flutterPagPlayer = layerMap.remove(getTextureId(call)); + if (flutterPagPlayer != null) { + flutterPagPlayer.stop(); + flutterPagPlayer.release(); + } + + TextureRegistry.SurfaceTextureEntry entry = entryMap.remove(getTextureId(call)); + if (entry != null) { + entry.release(); + } + } + } + + List getLayersUnderPoint(MethodCall call) { + FlutterPagPlayer flutterPagPlayer = getFlutterPagPlayer(call); + + List layerNames = new ArrayList<>(); + PAGLayer[] layers = null; + if (flutterPagPlayer != null) { + layers = flutterPagPlayer.getLayersUnderPoint( + ((Double) call.argument(_argumentPointX)).floatValue(), ((Double) call.argument(_argumentPointY)).floatValue()); + } + + if (layers != null) { + for (PAGLayer layer : layers) { + layerNames.add(layer.layerName()); + } + } + + return layerNames; + } + + FlutterPagPlayer getFlutterPagPlayer(MethodCall call) { + return layerMap.get(getTextureId(call)); + } + + String getTextureId(MethodCall call) { + return "" + call.argument(_argumentTextureId); + } + + //插件销毁 + public void onDestroy() { + releaseAll(); + channel.setMethodCallHandler(null); + } + + // 释放全部资源 + public void releaseAll() { + for (FlutterPagPlayer pagPlayer : layerMap.values()) { + pagPlayer.release(); + } + for (TextureRegistry.SurfaceTextureEntry entry : entryMap.values()) { + entry.release(); + } + freeEntryPool.clear(); + layerMap.clear(); + entryMap.clear(); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + onDestroy(); + } +} diff --git a/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/WorkThreadExecutor.java b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/WorkThreadExecutor.java new file mode 100644 index 0000000..987637d --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/WorkThreadExecutor.java @@ -0,0 +1,51 @@ +package com.example.flutter_pag_plugin; + +import android.os.Handler; +import android.os.HandlerThread; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + + +public class WorkThreadExecutor { + private static volatile WorkThreadExecutor instance; + private final ExecutorService executor; + private final ExecutorService singleThreadExecutor; + public static boolean multiThread = true; + private WorkThreadExecutor() { + executor = Executors.newCachedThreadPool(); + singleThreadExecutor = Executors.newSingleThreadExecutor(); + } + + public static WorkThreadExecutor getInstance() { + if (instance == null) { + synchronized (WorkThreadExecutor.class) { + if (instance == null) { + instance = new WorkThreadExecutor(); + } + } + } + return instance; + } + + public void enableMultiThread(boolean enabled) { + multiThread = enabled; + } + + public void post(Runnable task) { + if (multiThread) { + executor.execute(task); + } else { + task.run(); + } + } + + public void postInCertainThread(Runnable task) { + if (multiThread) { + executor.execute(task); + } else { + task.run(); + } + } + +} diff --git a/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/utils/EncodeUtil.java b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/utils/EncodeUtil.java new file mode 100644 index 0000000..3365a29 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/android/src/main/java/com/example/flutter_pag_plugin/utils/EncodeUtil.java @@ -0,0 +1,26 @@ +package com.example.flutter_pag_plugin.utils; + + +/** + * String 相关操作工具类 + * Created by Victorluo on 2016/8/11. + */ +public class EncodeUtil { + + public static String bytesToHexString(byte[] src) { + StringBuilder stringBuilder = new StringBuilder(""); + if (src == null || src.length <= 0) { + return null; + } + for (int i = 0; i < src.length; i++) { + int v = src[i] & 0xFF; + String hv = Integer.toHexString(v); + if (hv.length() < 2) { + stringBuilder.append(0); + } + stringBuilder.append(hv); + } + return stringBuilder.toString(); + } + +} diff --git a/local_packages/pag-1.0.7-patched/example/README.md b/local_packages/pag-1.0.7-patched/example/README.md new file mode 100644 index 0000000..23fe346 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/README.md @@ -0,0 +1,16 @@ +# flutter_pag_plugin_example + +Demonstrates how to use the flutter_pag_plugin plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/local_packages/pag-1.0.7-patched/example/android/app/build.gradle b/local_packages/pag-1.0.7-patched/example/android/app/build.gradle new file mode 100644 index 0000000..5484e9f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/app/build.gradle @@ -0,0 +1,54 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.flutter_pag_plugin_example" + minSdkVersion 19 + targetSdkVersion 31 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +flutter { + source '../..' +} diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/debug/AndroidManifest.xml b/local_packages/pag-1.0.7-patched/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..eebe320 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/AndroidManifest.xml b/local_packages/pag-1.0.7-patched/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..91e3ebf --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/java/com/example/flutter_pag_plugin_example/MainActivity.java b/local_packages/pag-1.0.7-patched/example/android/app/src/main/java/com/example/flutter_pag_plugin_example/MainActivity.java new file mode 100644 index 0000000..2c6b0fe --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/app/src/main/java/com/example/flutter_pag_plugin_example/MainActivity.java @@ -0,0 +1,6 @@ +package com.example.flutter_pag_plugin_example; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { +} diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/drawable-v21/launch_background.xml b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/drawable/launch_background.xml b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/values-night/styles.xml b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/values/styles.xml b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d74aa35 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/android/app/src/profile/AndroidManifest.xml b/local_packages/pag-1.0.7-patched/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..eebe320 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/local_packages/pag-1.0.7-patched/example/android/build.gradle b/local_packages/pag-1.0.7-patched/example/android/build.gradle new file mode 100644 index 0000000..7fe1f18 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/build.gradle @@ -0,0 +1,27 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/local_packages/pag-1.0.7-patched/example/android/gradle.properties b/local_packages/pag-1.0.7-patched/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/local_packages/pag-1.0.7-patched/example/android/gradle/wrapper/gradle-wrapper.properties b/local_packages/pag-1.0.7-patched/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bc6a58a --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/local_packages/pag-1.0.7-patched/example/android/settings.gradle b/local_packages/pag-1.0.7-patched/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/local_packages/pag-1.0.7-patched/example/data/0.pag b/local_packages/pag-1.0.7-patched/example/data/0.pag new file mode 100644 index 0000000..6d41bb0 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/0.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/1.pag b/local_packages/pag-1.0.7-patched/example/data/1.pag new file mode 100644 index 0000000..daaa914 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/1.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/10.pag b/local_packages/pag-1.0.7-patched/example/data/10.pag new file mode 100644 index 0000000..b9e6adf Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/10.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/11.pag b/local_packages/pag-1.0.7-patched/example/data/11.pag new file mode 100644 index 0000000..0dfd525 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/11.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/12.pag b/local_packages/pag-1.0.7-patched/example/data/12.pag new file mode 100644 index 0000000..c61ae6e Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/12.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/13.pag b/local_packages/pag-1.0.7-patched/example/data/13.pag new file mode 100644 index 0000000..7b795a5 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/13.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/14.pag b/local_packages/pag-1.0.7-patched/example/data/14.pag new file mode 100644 index 0000000..a686954 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/14.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/15.pag b/local_packages/pag-1.0.7-patched/example/data/15.pag new file mode 100644 index 0000000..92ff37b Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/15.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/16.pag b/local_packages/pag-1.0.7-patched/example/data/16.pag new file mode 100644 index 0000000..1780de1 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/16.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/17.pag b/local_packages/pag-1.0.7-patched/example/data/17.pag new file mode 100644 index 0000000..f6d7a48 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/17.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/18.pag b/local_packages/pag-1.0.7-patched/example/data/18.pag new file mode 100644 index 0000000..eeb7af6 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/18.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/19.pag b/local_packages/pag-1.0.7-patched/example/data/19.pag new file mode 100644 index 0000000..c61ae6e Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/19.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/2.pag b/local_packages/pag-1.0.7-patched/example/data/2.pag new file mode 100644 index 0000000..da6525c Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/2.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/3.pag b/local_packages/pag-1.0.7-patched/example/data/3.pag new file mode 100644 index 0000000..512a61c Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/3.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/4.pag b/local_packages/pag-1.0.7-patched/example/data/4.pag new file mode 100644 index 0000000..24cddb4 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/4.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/5.pag b/local_packages/pag-1.0.7-patched/example/data/5.pag new file mode 100644 index 0000000..e356cb1 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/5.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/6.pag b/local_packages/pag-1.0.7-patched/example/data/6.pag new file mode 100644 index 0000000..942857f Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/6.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/7.pag b/local_packages/pag-1.0.7-patched/example/data/7.pag new file mode 100644 index 0000000..359a0ef Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/7.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/8.pag b/local_packages/pag-1.0.7-patched/example/data/8.pag new file mode 100644 index 0000000..2c4d4c0 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/8.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/9.pag b/local_packages/pag-1.0.7-patched/example/data/9.pag new file mode 100644 index 0000000..afca9a0 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/9.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/dance.pag b/local_packages/pag-1.0.7-patched/example/data/dance.pag new file mode 100644 index 0000000..6446ae1 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/dance.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/fans.pag b/local_packages/pag-1.0.7-patched/example/data/fans.pag new file mode 100644 index 0000000..8087896 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/fans.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/large.pag b/local_packages/pag-1.0.7-patched/example/data/large.pag new file mode 100644 index 0000000..2ff4036 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/large.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/data/test.png b/local_packages/pag-1.0.7-patched/example/data/test.png new file mode 100644 index 0000000..790134a Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/data/test.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Flutter/AppFrameworkInfo.plist b/local_packages/pag-1.0.7-patched/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Flutter/Debug.xcconfig b/local_packages/pag-1.0.7-patched/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/local_packages/pag-1.0.7-patched/example/ios/Flutter/Release.xcconfig b/local_packages/pag-1.0.7-patched/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/local_packages/pag-1.0.7-patched/example/ios/Podfile b/local_packages/pag-1.0.7-patched/example/ios/Podfile new file mode 100644 index 0000000..6e683d1 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Podfile @@ -0,0 +1,39 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + pod 'libpag', '4.3.68' + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/local_packages/pag-1.0.7-patched/example/ios/Podfile.lock b/local_packages/pag-1.0.7-patched/example/ios/Podfile.lock new file mode 100644 index 0000000..fff7bbf --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Podfile.lock @@ -0,0 +1,30 @@ +PODS: + - Flutter (1.0.0) + - libpag (4.2.11) + - pag (0.0.1): + - Flutter + - libpag + +DEPENDENCIES: + - Flutter (from `Flutter`) + - libpag (= 4.2.11) + - pag (from `.symlinks/plugins/pag/ios`) + +SPEC REPOS: + trunk: + - libpag + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + pag: + :path: ".symlinks/plugins/pag/ios" + +SPEC CHECKSUMS: + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + libpag: 38a4633825d5dd8161c4e84ca3469395f18fd639 + pag: ff30ba5e4be59380a5ddac6b183905729be15a3c + +PODFILE CHECKSUM: 535ba81be026f5bbd306994284bed8b494a6faf5 + +COCOAPODS: 1.11.2 diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.pbxproj b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..652d85b --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,554 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F508DC21C3FF0565306F1BA4 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4669C38C14B0AB5FAC6611C9 /* libPods-Runner.a */; }; + FB2F7A2327DB72F100A90D02 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FB2F7A2227DB72F100A90D02 /* libc++.tbd */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3DBDC017D50B82427AE7948E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 4669C38C14B0AB5FAC6611C9 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E8A1C220649E233F26806054 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F16F7F92BD31D7A2AED62065 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + FB2F7A2227DB72F100A90D02 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FB2F7A2327DB72F100A90D02 /* libc++.tbd in Frameworks */, + F508DC21C3FF0565306F1BA4 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 01346A5F62851C62F25D73AE /* Frameworks */ = { + isa = PBXGroup; + children = ( + FB2F7A2227DB72F100A90D02 /* libc++.tbd */, + 4669C38C14B0AB5FAC6611C9 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 015AA143B0162E4AB723C99A /* Pods */ = { + isa = PBXGroup; + children = ( + F16F7F92BD31D7A2AED62065 /* Pods-Runner.debug.xcconfig */, + E8A1C220649E233F26806054 /* Pods-Runner.release.xcconfig */, + 3DBDC017D50B82427AE7948E /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 015AA143B0162E4AB723C99A /* Pods */, + 01346A5F62851C62F25D73AE /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 5F524B1F47B60F9D6A784898 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + AE30BE6D72416E328EEC3D7A /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 5F524B1F47B60F9D6A784898 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + AE30BE6D72416E328EEC3D7A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 93R838NHT9; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPagPluginExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 93R838NHT9; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPagPluginExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 93R838NHT9; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPagPluginExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/AppDelegate.h b/local_packages/pag-1.0.7-patched/example/ios/Runner/AppDelegate.h new file mode 100644 index 0000000..c73979a --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner/AppDelegate.h @@ -0,0 +1,13 @@ +// +// AppDelegate.h +// AppDelegate +// +// Created by jenmalli on 2022/3/16. +// Copyright © 2022 Tencent. All rights reserved. +// +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/AppDelegate.m b/local_packages/pag-1.0.7-patched/example/ios/Runner/AppDelegate.m new file mode 100644 index 0000000..5526183 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner/AppDelegate.m @@ -0,0 +1,20 @@ +// +// AppDelegate.m +// AppDelegate +// +// Created by jenmalli on 2022/3/16. +// Copyright © 2022 Tencent. All rights reserved. +// +#import "AppDelegate.h" +#import "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/local_packages/pag-1.0.7-patched/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Base.lproj/Main.storyboard b/local_packages/pag-1.0.7-patched/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/Info.plist b/local_packages/pag-1.0.7-patched/example/ios/Runner/Info.plist new file mode 100644 index 0000000..36a04dc --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_pag_plugin_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/local_packages/pag-1.0.7-patched/example/ios/Runner/main.m b/local_packages/pag-1.0.7-patched/example/ios/Runner/main.m new file mode 100644 index 0000000..dff6597 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ios/Runner/main.m @@ -0,0 +1,9 @@ +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/local_packages/pag-1.0.7-patched/example/lib/main.dart b/local_packages/pag-1.0.7-patched/example/lib/main.dart new file mode 100644 index 0000000..aa02ebb --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/lib/main.dart @@ -0,0 +1,319 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pag/pag.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + /// use codes below to config cache / multi-thread settings + // PAG.enableCache(false); + // PAG.enableMultiThread(false); + // PAG.setCacheSize(4); ///default size is 8 + return MaterialApp( + home: MyHome(), + ); + } +} + +class MyHome extends StatefulWidget { + @override + _MyHomeState createState() => _MyHomeState(); +} + +///PAG用于ListView,用于测试加载速度 +class _MyListHomeState extends State { + bool visible = false; + @override + Widget build(BuildContext context) { + return Column( + children: [ + TextButton( + child: Container( + width: 100, + height: 50, + alignment: Alignment.center, + color: Color.fromARGB(255, 100, 255, 100), + child: Text( + 'Test' + ), + ), + onPressed: (){ + setState(() { + visible = !visible; + }); + }, + + ), + Expanded( + child: Visibility( + visible: visible, + child: ListView.builder( + itemCount: 300, + // cacheExtent: 3000.0, + itemBuilder: (context, index) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PAGView.asset( + 'data/${index%20}.pag', + width: (index % 7) * 20 + 10, + height: (index % 7) * 20 + 10, + // 'data/large.pag', + repeatCount: PAGView.REPEAT_COUNT_LOOP, + initProgress: 0.25, + autoPlay: true, + key: ValueKey(index), + ), + ], + ); + } + ), + ) + ), + ], + ); + } + +} + +class _MyHomeState extends State { + + GlobalKey _fansDanceKey = GlobalKey(debugLabel: _assetFans); + GlobalKey _assetDanceKey = GlobalKey(debugLabel: _assetFans); + GlobalKey get assetPagKey => _pagAsset == _assetFans ? _fansDanceKey : _assetDanceKey; + + final GlobalKey networkPagKey = GlobalKey(); + final GlobalKey bytesPagKey = GlobalKey(); + + Uint8List? bytesData; + + // 本地加载资源 + static const String _assetFans = 'data/fans.pag'; + static const String _assetDance = 'data/dance.pag'; + static const String _assetError = 'data/error.pag'; + String _pagAsset = _assetFans; + + void changeAsset() { + setState(() { + _pagAsset = _pagAsset == _assetFans ? _assetDance : _assetFans; + }); + } + + @override + void initState() { + rootBundle.load("data/fans.pag").then((data) { + setState(() { + bytesData = Uint8List.view(data.buffer); + }); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('PAGView example app'), + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ///TODO: PAGView加载本地资源 + Padding( + padding: EdgeInsets.only(top: 20, left: 12, bottom: 20), + child: Text( + "PAGView加载本地资源:", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black54), + ), + ), + SizedBox( + width: 100, + height: 100, + child: PAGView.asset( + _pagAsset, + repeatCount: PAGView.REPEAT_COUNT_LOOP, + initProgress: 0.25, + autoPlay: true, + key: assetPagKey, + ), + ), + Padding( + padding: EdgeInsets.only(left: 12, top: 10), + child: Row( + children: [ + IconButton( + iconSize: 30, + icon: const Icon( + Icons.pause_circle, + color: Colors.black54, + ), + onPressed: () { + // 暂停 + assetPagKey.currentState?.pause(); + }, + ), + IconButton( + iconSize: 30, + icon: const Icon( + Icons.play_circle, + color: Colors.black54, + ), + onPressed: () { + //播放 + assetPagKey.currentState?.start(); + }, + ), + IconButton( + iconSize: 30, + icon: const Icon( + Icons.published_with_changes_sharp, + color: Colors.black54, + ), + onPressed: changeAsset), + Text( + "<= 请点击控制动画(可切换)", + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black54), + ), + ], + ), + ), + + /// TODO: PAGView加载网络资源 + Padding( + padding: EdgeInsets.only(top: 50, left: 12, bottom: 20), + child: Text( + "PAGView加载网络资源:", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black54), + ), + ), + PAGView.network( + "https://svipwebwx-30096.sz.gfp.tencent-cloud.com/file1647585475981.pag", + repeatCount: PAGView.REPEAT_COUNT_LOOP, + initProgress: 0.25, + autoPlay: true, + key: networkPagKey, + ), + Padding( + padding: EdgeInsets.only(left: 12, top: 10), + child: Row( + children: [ + IconButton( + iconSize: 30, + icon: const Icon( + Icons.pause_circle, + color: Colors.black54, + ), + onPressed: () { + // 暂停 + networkPagKey.currentState?.pause(); + }, + ), + IconButton( + iconSize: 30, + icon: const Icon( + Icons.play_circle, + color: Colors.black54, + ), + onPressed: () { + // 播放 + networkPagKey.currentState?.start(); + }, + ), + Text( + "<= 请点击控制动画", + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black54), + ), + ], + ), + ), + + /// TODO: PAGView加载二进制资源 + Padding( + padding: EdgeInsets.only(top: 50, left: 12, bottom: 20), + child: Text( + "PAGView加载二进制资源:", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black54), + ), + ), + Visibility( + visible: bytesData?.isNotEmpty == true, + child: PAGView.bytes( + bytesData, + repeatCount: PAGView.REPEAT_COUNT_LOOP, + initProgress: 0.25, + autoPlay: true, + key: bytesPagKey, + )), + Padding( + padding: EdgeInsets.only(left: 12, top: 10), + child: Row( + children: [ + IconButton( + iconSize: 30, + icon: const Icon( + Icons.pause_circle, + color: Colors.black54, + ), + onPressed: () { + // 暂停 + bytesPagKey.currentState?.pause(); + }, + ), + IconButton( + iconSize: 30, + icon: const Icon( + Icons.play_circle, + color: Colors.black54, + ), + onPressed: () { + // 播放 + bytesPagKey.currentState?.start(); + }, + ), + Text( + "<= 请点击控制动画", + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black54), + ), + ], + ), + ), + + + /// TODO: PAGView加载二进制资源 + Padding( + padding: EdgeInsets.only(top: 20, left: 12, bottom: 20), + child: Text( + "PAGView加载失败的默认占位图:", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black54), + ), + ), + SizedBox( + width: 100, + height: 100, + child: PAGView.asset( + _assetError, + repeatCount: PAGView.REPEAT_COUNT_LOOP, + initProgress: 0.25, + autoPlay: true, + defaultBuilder: (context){ + return Container( + color: Colors.grey, + alignment: Alignment.center, + margin: EdgeInsets.all(16), + child: Text("load fail"), + ); + }, + ), + ), + ], + ), + )); + } +} diff --git a/local_packages/pag-1.0.7-patched/example/ohos/AppScope/app.json5 b/local_packages/pag-1.0.7-patched/example/ohos/AppScope/app.json5 new file mode 100644 index 0000000..4ebc3fa --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.tencent.flutter_pag_plugin_example", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/AppScope/resources/base/element/string.json b/local_packages/pag-1.0.7-patched/example/ohos/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..dd97bef --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "flutter_pag_plugin_example" + } + ] +} diff --git a/local_packages/pag-1.0.7-patched/example/ohos/AppScope/resources/base/media/app_icon.png b/local_packages/pag-1.0.7-patched/example/ohos/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000..ce307a8 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ohos/AppScope/resources/base/media/app_icon.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ohos/build-profile.json5 b/local_packages/pag-1.0.7-patched/example/ohos/build-profile.json5 new file mode 100644 index 0000000..0c7cd76 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/build-profile.json5 @@ -0,0 +1,34 @@ +{ + "app": { + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "5.0.0(12)", + "runtimeOS": "HarmonyOS" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, + { + "name": "pag", + "srcPath": "../../ohos" + }, + { + "name": "libpag", + "srcPath": "../../../libpag/ohos/libpag" + } + ] +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/build-profile.json5 b/local_packages/pag-1.0.7-patched/example/ohos/entry/build-profile.json5 new file mode 100644 index 0000000..633d360 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/build-profile.json5 @@ -0,0 +1,29 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +{ + "apiType": 'stageMode', + "buildOption": { + }, + "targets": [ + { + "name": "default", + "runtimeOS": "HarmonyOS" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/hvigorfile.ts b/local_packages/pag-1.0.7-patched/example/ohos/entry/hvigorfile.ts new file mode 100644 index 0000000..894fc15 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/hvigorfile.ts @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +export { hapTasks } from '@ohos/hvigor-ohos-plugin'; diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/oh-package.json5 b/local_packages/pag-1.0.7-patched/example/ohos/entry/oh-package.json5 new file mode 100644 index 0000000..60efeb6 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "pag": "file:../har/pag.har" + } +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/ets/entryability/EntryAbility.ets b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000..8bc48be --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,24 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos'; +import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant'; + +export default class EntryAbility extends FlutterAbility { + configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + GeneratedPluginRegistrant.registerWith(flutterEngine) + } +} diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/ets/pages/Index.ets b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..1125f9f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import common from '@ohos.app.ability.common'; +import { FlutterPage } from '@ohos/flutter_ohos' + +let storage = LocalStorage.getShared() +const EVENT_BACK_PRESS = 'EVENT_BACK_PRESS' + +@Entry(storage) +@Component +struct Index { + private context = getContext(this) as common.UIAbilityContext + @LocalStorageLink('viewId') viewId: string = ""; + + build() { + Column() { + FlutterPage({ viewId: this.viewId }) + } + } + + onBackPress(): boolean { + this.context.eventHub.emit(EVENT_BACK_PRESS) + return true + } +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets new file mode 100644 index 0000000..1c9dc0c --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets @@ -0,0 +1,26 @@ +import { FlutterEngine, Log } from '@ohos/flutter_ohos'; +import FlutterPagPlugin from 'pag'; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Ohos platform. + */ + +const TAG = "GeneratedPluginRegistrant"; + +export class GeneratedPluginRegistrant { + + static registerWith(flutterEngine: FlutterEngine) { + try { + flutterEngine.getPlugins()?.add(new FlutterPagPlugin()); + } catch (e) { + Log.e( + TAG, + "Tried to register plugins with FlutterEngine (" + + flutterEngine + + ") failed."); + Log.e(TAG, "Received exception while registering", e); + } + } +} diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/module.json5 b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/module.json5 new file mode 100644 index 0000000..7bbf78b --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/module.json5 @@ -0,0 +1,53 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:icon", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ], + "requestPermissions": [ + {"name" : "ohos.permission.INTERNET"}, + ] + } +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/element/color.json b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000..3c71296 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/element/string.json b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000..702450e --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "flutter_pag_plugin_example" + } + ] +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/media/icon.png b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/media/icon.png new file mode 100644 index 0000000..ce307a8 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/media/icon.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/profile/main_pages.json b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..1898d94 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/en_US/element/string.json b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000..702450e --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "flutter_pag_plugin_example" + } + ] +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/rawfile/large.pag b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/rawfile/large.pag new file mode 100644 index 0000000..2ff4036 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/rawfile/large.pag differ diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/zh_CN/element/string.json b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000..dd83ca2 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "模块描述" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "flutter_pag_plugin_example" + } + ] +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/test/Ability.test.ets b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000..25d4c71 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,50 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import hilog from '@ohos.hilog'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium' + +export default function abilityTest() { + describe('ActsAbilityTest', function () { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(function () { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(function () { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(function () { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(function () { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain',0, function () { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc' + let b = 'b' + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b) + expect(a).assertEqual(a) + }) + }) +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/test/List.test.ets b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000..f414003 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,20 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import abilityTest from './Ability.test' + +export default function testsuite() { + abilityTest() +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/testability/TestAbility.ets b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/testability/TestAbility.ets new file mode 100644 index 0000000..4ca645e --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/testability/TestAbility.ets @@ -0,0 +1,63 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import UIAbility from '@ohos.app.ability.UIAbility'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import hilog from '@ohos.hilog'; +import { Hypium } from '@ohos/hypium'; +import testsuite from '../test/List.test'; +import window from '@ohos.window'; + +export default class TestAbility extends UIAbility { + onCreate(want, launchParam) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onCreate'); + hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? ''); + hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:'+ JSON.stringify(launchParam) ?? ''); + var abilityDelegator: any + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() + var abilityDelegatorArguments: any + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() + hilog.info(0x0000, 'testTag', '%{public}s', 'start run testcase!!!'); + Hypium.hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite) + } + + onDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageCreate'); + windowStage.loadContent('testability/pages/Index', (err, data) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', + JSON.stringify(data) ?? ''); + }); + } + + onWindowStageDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy'); + } + + onForeground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground'); + } + + onBackground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground'); + } +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets new file mode 100644 index 0000000..cef0447 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets @@ -0,0 +1,49 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import hilog from '@ohos.hilog'; + +@Entry +@Component +struct Index { + aboutToAppear() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility index aboutToAppear'); + } + @State message: string = 'Hello World' + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + Button() { + Text('next page') + .fontSize(20) + .fontWeight(FontWeight.Bold) + }.type(ButtonType.Capsule) + .margin({ + top: 20 + }) + .backgroundColor('#0D9FFB') + .width('35%') + .height('5%') + .onClick(()=>{ + }) + } + .width('100%') + } + .height('100%') + } + } \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts new file mode 100644 index 0000000..1def08f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts @@ -0,0 +1,64 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import hilog from '@ohos.hilog'; +import TestRunner from '@ohos.application.testRunner'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; + +var abilityDelegator = undefined +var abilityDelegatorArguments = undefined + +async function onAbilityCreateCallback() { + hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback'); +} + +async function addAbilityMonitorCallback(err: any) { + hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? ''); +} + +export default class OpenHarmonyTestRunner implements TestRunner { + constructor() { + } + + onPrepare() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare '); + } + + async onRun() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run'); + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() + var testAbilityName = abilityDelegatorArguments.bundleName + '.TestAbility' + let lMonitor = { + abilityName: testAbilityName, + onAbilityCreate: onAbilityCreateCallback, + }; + abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback) + var cmd = 'aa start -d 0 -a TestAbility' + ' -b ' + abilityDelegatorArguments.bundleName + var debug = abilityDelegatorArguments.parameters['-D'] + if (debug == 'true') + { + cmd += ' -D' + } + hilog.info(0x0000, 'testTag', 'cmd : %{public}s', cmd); + abilityDelegator.executeShellCommand(cmd, + (err: any, d: any) => { + hilog.info(0x0000, 'testTag', 'executeShellCommand : err : %{public}s', JSON.stringify(err) ?? ''); + hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.stdResult ?? ''); + hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.exitCode ?? ''); + }) + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end'); + } +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/module.json5 b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000..fab77ce --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/module.json5 @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +{ + "module": { + "name": "entry_test", + "type": "feature", + "description": "$string:module_test_desc", + "mainElement": "TestAbility", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:test_pages", + "abilities": [ + { + "name": "TestAbility", + "srcEntry": "./ets/testability/TestAbility.ets", + "description": "$string:TestAbility_desc", + "icon": "$media:icon", + "label": "$string:TestAbility_label", + "exported": true, + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "skills": [ + { + "actions": [ + "action.system.home" + ], + "entities": [ + "entity.system.home" + ] + } + ] + } + ] + } +} diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/element/color.json b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/element/color.json new file mode 100644 index 0000000..3c71296 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/element/string.json b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/element/string.json new file mode 100644 index 0000000..65d8fa5 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_test_desc", + "value": "test ability description" + }, + { + "name": "TestAbility_desc", + "value": "the test ability" + }, + { + "name": "TestAbility_label", + "value": "test label" + } + ] +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/media/icon.png b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/media/icon.png new file mode 100644 index 0000000..ce307a8 Binary files /dev/null and b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/media/icon.png differ diff --git a/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/profile/test_pages.json b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/profile/test_pages.json new file mode 100644 index 0000000..b7e7343 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/entry/src/ohosTest/resources/base/profile/test_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "testability/pages/Index" + ] +} diff --git a/local_packages/pag-1.0.7-patched/example/ohos/hvigor/hvigor-config.json5 b/local_packages/pag-1.0.7-patched/example/ohos/hvigor/hvigor-config.json5 new file mode 100644 index 0000000..541ba35 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/hvigor/hvigor-config.json5 @@ -0,0 +1,20 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +{ + "modelVersion": "5.0.0", + "dependencies": { + } +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/hvigorfile.ts b/local_packages/pag-1.0.7-patched/example/ohos/hvigorfile.ts new file mode 100644 index 0000000..8f2d2aa --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/hvigorfile.ts @@ -0,0 +1,21 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/ohos/oh-package.json5 b/local_packages/pag-1.0.7-patched/example/ohos/oh-package.json5 new file mode 100644 index 0000000..2642683 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/ohos/oh-package.json5 @@ -0,0 +1,21 @@ +{ + "modelVersion": "5.0.0", + "name": "flutter_pag_plugin_example", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "@ohos/flutter_ohos": "file:./har/flutter.har" + }, + "devDependencies": { + "@ohos/hypium": "1.0.6" + }, + "overrides": { + "@ohos/flutter_ohos": "file:./har/flutter.har", + "@ohos/flutter_module": "file:./entry", + "pag": "file:../../ohos", + "@tencent/libpag": "file:../../../libpag/ohos/libpag" + } +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/example/pubspec.yaml b/local_packages/pag-1.0.7-patched/example/pubspec.yaml new file mode 100644 index 0000000..afd1204 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/pubspec.yaml @@ -0,0 +1,72 @@ +name: flutter_pag_plugin_example +description: Demonstrates how to use the flutter_pag_plugin plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + pag: + # When depending on this package from a real application you should use: + # flutter_pag_plugin: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - data/ + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/local_packages/pag-1.0.7-patched/example/test/widget_test.dart b/local_packages/pag-1.0.7-patched/example/test/widget_test.dart new file mode 100644 index 0000000..ccc9a8b --- /dev/null +++ b/local_packages/pag-1.0.7-patched/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:flutter_pag_plugin_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/local_packages/pag-1.0.7-patched/ios/Classes/FlutterPagPlugin.h b/local_packages/pag-1.0.7-patched/ios/Classes/FlutterPagPlugin.h new file mode 100644 index 0000000..50f81ec --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ios/Classes/FlutterPagPlugin.h @@ -0,0 +1,11 @@ +// +// FlutterPagPlugin.h +// FlutterPagPlugin +// +// Created by 黎敬茂 on 2022/3/14. +// Copyright © 2022 Tencent. All rights reserved. +// +#import + +@interface FlutterPagPlugin : NSObject +@end diff --git a/local_packages/pag-1.0.7-patched/ios/Classes/FlutterPagPlugin.m b/local_packages/pag-1.0.7-patched/ios/Classes/FlutterPagPlugin.m new file mode 100644 index 0000000..7429a4e --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ios/Classes/FlutterPagPlugin.m @@ -0,0 +1,258 @@ +// +// FlutterPagPlugin.m +// FlutterPagPlugin +// +// Created by 黎敬茂 on 2022/3/14. +// Copyright © 2022 Tencent. All rights reserved. +// +#import "FlutterPagPlugin.h" +#import "TGFlutterPagRender.h" +#import "TGFlutterPagDownloadManager.h" + +/** + FlutterPagPlugin,处理flutter MethodChannel约定的方法 + */ +#define PlayCallback @"PAGCallback" +#define ArgumentEvent @"PAGEvent" +#define ArgumentTextureId @"textureId" + +@interface FlutterPagPlugin() + +/// flutter引擎注册的textures对象 +@property(nonatomic, weak) NSObject* textures; + +/// flutter引擎注册的registrar对象 +@property(nonatomic, weak) NSObject* registrar; + +/// 保存textureId跟render对象的对应关系 +@property (nonatomic, strong) NSMutableDictionary *renderMap; + +/// pag对象的缓存 +@property (nonatomic, strong)NSCache *cache; + +/// 用于通信的channel +@property (nonatomic, strong)FlutterMethodChannel* channel; + +@end + +@implementation FlutterPagPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:@"flutter_pag_plugin" + binaryMessenger:[registrar messenger]]; + FlutterPagPlugin* instance = [[FlutterPagPlugin alloc] init]; + instance.textures = registrar.textures; + instance.registrar = registrar; + instance.channel = channel; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (void)getLayersUnderPoint:(id)arguments result:(FlutterResult _Nonnull)result { + NSNumber* textureId = arguments[@"textureId"]; + NSNumber* x = arguments[@"x"]; + NSNumber* y = arguments[@"y"]; + TGFlutterPagRender *render = [_renderMap objectForKey:textureId]; + NSArray *names = [render getLayersUnderPoint:CGPointMake(x.doubleValue, y.doubleValue)]; + result(names); +} + +- (void)release:(id)arguments result:(FlutterResult _Nonnull)result { + NSNumber* textureId = arguments[@"textureId"]; + if(textureId == nil){ + result(@""); + return; + } + [self.textures unregisterTexture:textureId.intValue]; + TGFlutterPagRender *render = [_renderMap objectForKey:textureId]; + [render releaseRender]; + [_renderMap removeObjectForKey:textureId]; + result(@""); +} + +- (void)setProgress:(id)arguments result:(FlutterResult _Nonnull)result { + NSNumber* textureId = arguments[@"textureId"]; + if(textureId == nil){ + result(@""); + return; + } + double progress = 0.0; + if (arguments[@"progress"]) { + progress = [arguments[@"progress"] doubleValue]; + } + TGFlutterPagRender *render = [_renderMap objectForKey:textureId]; + [render setProgress:progress]; + result(@""); +} + +- (void)pause:(id)arguments result:(FlutterResult _Nonnull)result { + NSNumber* textureId = arguments[@"textureId"]; + if(textureId == nil){ + result(@""); + return; + } + TGFlutterPagRender *render = [_renderMap objectForKey:textureId]; + [render pauseRender]; + result(@""); +} + +- (void)stop:(id)arguments result:(FlutterResult _Nonnull)result { + NSNumber* textureId = arguments[@"textureId"]; + if(textureId == nil){ + result(@""); + return; + } + TGFlutterPagRender *render = [_renderMap objectForKey:textureId]; + [render stopRender]; + result(@""); +} + +- (void)start:(id)arguments result:(FlutterResult _Nonnull)result { + NSNumber* textureId = arguments[@"textureId"]; + if(textureId == nil){ + result(@""); + return; + } + TGFlutterPagRender *render = [_renderMap objectForKey:textureId]; + [render startRender]; + result(@""); +} + +- (void)initPag:(id)arguments result:(FlutterResult _Nonnull)result { + if (arguments == nil || (arguments[@"assetName"] == NSNull.null && arguments[@"url"] == NSNull.null && arguments[@"bytesData"] == NSNull.null)) { + result(@-1); + NSLog(@"showPag arguments is nil"); + return; + } + double initProgress = 0.0; + if (arguments[@"initProgress"]) { + initProgress = [arguments[@"initProgress"] doubleValue]; + } + int repeatCount = -1; + if(arguments[@"repeatCount"]){ + repeatCount = [[arguments objectForKey:@"repeatCount"] intValue]; + } + + BOOL autoPlay = NO; + if(arguments[@"autoPlay"]){ + autoPlay = [[arguments objectForKey:@"autoPlay"] boolValue]; + } + + NSString* assetName = arguments[@"assetName"]; + NSData *pagData = nil; + if ([assetName isKindOfClass:NSString.class] && assetName.length > 0) { + NSString *key = assetName; + pagData = [self getCacheData:key]; + if (!pagData) { + NSString* package = arguments[@"package"]; + NSString* resourcePath; + if(package && [package isKindOfClass:NSString.class] && package.length > 0){ + resourcePath = [self.registrar lookupKeyForAsset:assetName fromPackage:package]; + }else{ + resourcePath = [self.registrar lookupKeyForAsset:assetName]; + } + + resourcePath = [[NSBundle mainBundle] pathForResource:resourcePath ofType:nil]; + + pagData = [NSData dataWithContentsOfFile:resourcePath]; + [self setCacheData:key data:pagData]; + + } + [self pagRenderWithPagData:pagData progress:initProgress repeatCount:repeatCount autoPlay:autoPlay result:result]; + } + NSString* url = arguments[@"url"]; + if ([url isKindOfClass:NSString.class] && url.length > 0) { + NSURLSessionDownloadTask *task; + [task resume]; + NSString *key = url; + pagData = [self getCacheData:key]; + if (!pagData) { + __weak typeof(self) weak_self = self; + [TGFlutterPagDownloadManager download:url completionHandler:^(NSData * _Nonnull data, NSError * _Nonnull error) { + if (data) { + [weak_self setCacheData:key data:pagData]; + [weak_self pagRenderWithPagData:data progress:initProgress repeatCount:repeatCount autoPlay:autoPlay result:result]; + }else{ + result(@-1); + } + }]; + }else{ + [self pagRenderWithPagData:pagData progress:initProgress repeatCount:repeatCount autoPlay:autoPlay result:result]; + } + } + + id bytesData = arguments[@"bytesData"]; + if(bytesData != nil && [bytesData isKindOfClass:FlutterStandardTypedData.class]){ + FlutterStandardTypedData *typedData = bytesData; + if(typedData.type == FlutterStandardDataTypeUInt8 && typedData.data != nil){ + [self pagRenderWithPagData:typedData.data progress:initProgress repeatCount:repeatCount autoPlay:autoPlay result:result]; + }else{ + result(@-1); + } + } +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + + id arguments = call.arguments; + if ([@"getPlatformVersion" isEqualToString:call.method]) { + result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); + } else if([@"initPag" isEqualToString:call.method]){ + [self initPag:arguments result:result]; + } else if([@"start" isEqualToString:call.method]){ + [self start:arguments result:result]; + } else if([@"stop" isEqualToString:call.method]){ + [self stop:arguments result:result]; + } else if([@"pause" isEqualToString:call.method]){ + [self pause:arguments result:result]; + } else if([@"setProgress" isEqual:call.method]){ + [self setProgress:arguments result:result]; + } else if([@"release" isEqualToString:call.method]){ + [self release:arguments result:result]; + } else if([@"getLayersUnderPoint" isEqualToString:call.method]){ + [self getLayersUnderPoint:arguments result:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +-(void)pagRenderWithPagData:(NSData *)pagData progress:(double)progress repeatCount:(int)repeatCount autoPlay:(BOOL)autoPlay result:(FlutterResult)result{ + __block int64_t textureId = -1; + __weak typeof(self) weakSelf = self; + TGFlutterPagRender *render = [[TGFlutterPagRender alloc] initWithPagData:pagData progress:progress frameUpdateCallback:^{ + [weakSelf.textures textureFrameAvailable:textureId]; + } eventCallback:^(NSString * event) { + [weakSelf.channel invokeMethod:PlayCallback arguments:@{ArgumentTextureId:@(textureId), ArgumentEvent:event}]; + }]; + [render setRepeatCount:repeatCount]; + textureId = [self.textures registerTexture:render]; + if(_renderMap == nil){ + _renderMap = [[NSMutableDictionary alloc] init]; + } + [_renderMap setObject:render forKey:@(textureId)]; + result(@{@"textureId":@(textureId), @"width":@([render size].width), @"height":@([render size].height)}); + if(autoPlay){ + [render startRender]; + } +} + +-(NSData *)getCacheData:(NSString *)key{ + return [self.cache objectForKey:key]; +} + +-(void)setCacheData:(NSString *)key data:(NSData *)data{ + if (data == nil || key == nil) { + return; + } + [self.cache setObject:data forKey:key cost:data.length]; +} + +-(NSCache *)cache{ + if (!_cache) { + _cache = [[NSCache alloc] init]; + ///缓存64m + _cache.totalCostLimit = 64*1024*1024; + _cache.countLimit = 32; + } + return _cache; +} +@end diff --git a/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagDownloadManager.h b/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagDownloadManager.h new file mode 100644 index 0000000..6b08eb5 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagDownloadManager.h @@ -0,0 +1,23 @@ +// +// TGFlutterPagDownloadManager.h +// flutter_pag_plugin +// +// Created by 黎敬茂 on 2022/3/14. +// Copyright © 2022 Tencent. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Pag文件下载工具 + */ +@interface TGFlutterPagDownloadManager : NSObject + +///下载pag文件 ++(void)download:(NSString *)urlStr completionHandler:(void (^)(NSData * data, NSError * error))handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagDownloadManager.m b/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagDownloadManager.m new file mode 100644 index 0000000..aeeec66 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagDownloadManager.m @@ -0,0 +1,81 @@ +// +// TGFlutterPagDownloadManager.m +// flutter_pag_plugin +// +// Created by 黎敬茂 on 2022/3/14. +// Copyright © 2022 Tencent. All rights reserved. +// + +#import "TGFlutterPagDownloadManager.h" +#import + +#define CACHE_DIR @"pagCache" + +@implementation TGFlutterPagDownloadManager + ++(void)download:(NSString *)urlStr completionHandler:(void (^)(NSData * data, NSError * error))handler{ + NSString *cachePath = [TGFlutterPagDownloadManager cachePath:urlStr]; + if(cachePath == nil){ + handler(nil, [[NSError alloc] initWithDomain:NSURLErrorDomain code:-1 userInfo:nil]); + return; + } + if ([[NSFileManager defaultManager] fileExistsAtPath:cachePath]) { + NSData *data = [NSData dataWithContentsOfFile:cachePath]; + if (data) { + handler(data, nil); + return; + } + + } + NSURL *url = [NSURL URLWithString:urlStr]; + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] + dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + handler(data, error); + if (data) { + [TGFlutterPagDownloadManager saveData:data path:cachePath]; + } + }]; + [dataTask resume]; +} + ++(NSString *)cacheDir{ + NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + static NSString *cacheDir; + if (!cacheDir) { + cacheDir= [NSString stringWithFormat:@"%@/%@", cachePaths.firstObject, CACHE_DIR]; + } + return cacheDir; +} + ++(NSString *)cachePath:(NSString *)url{ + if (![url isKindOfClass:NSString.class] || url.length <= 0) { + return nil; + } + + NSString *key = [TGFlutterPagDownloadManager md5:url]; + return [NSString stringWithFormat:@"%@/%@", [self cacheDir], key]; +} + ++(NSString *)md5:(NSString *)str{ + const char* input = [str UTF8String]; + unsigned char result[CC_MD5_DIGEST_LENGTH]; + CC_MD5(input, (CC_LONG)strlen(input), result); + + NSMutableString *digest = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2]; + for (NSInteger i = 0; i < CC_MD5_DIGEST_LENGTH; i++) { + [digest appendFormat:@"%02x", result[i]]; + } + return digest; +} + ++(void)saveData:(NSData *)data path:(NSString *)cachePath{ + NSString *cahceDir = [TGFlutterPagDownloadManager cacheDir]; + if (![[NSFileManager defaultManager] fileExistsAtPath:cachePath]) { + [[NSFileManager defaultManager] createDirectoryAtPath:cahceDir withIntermediateDirectories:YES attributes:nil error:nil]; + } + [data writeToFile:cachePath atomically:YES]; +} + +@end diff --git a/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagRender.h b/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagRender.h new file mode 100644 index 0000000..5f13fc4 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagRender.h @@ -0,0 +1,52 @@ +// +// TGFlutterPageRender.h +// Tgclub +// +// Created by 黎敬茂 on 2021/11/25. +// Copyright © 2021 Tencent. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#define EventStart @"onAnimationStart" +#define EventEnd @"onAnimationEnd" +#define EventCancel @"onAnimationCancel" +#define EventRepeat @"onAnimationRepeat" + +typedef void(^FrameUpdateCallback)(void); + +typedef void(^PAGEventCallback)(NSString *); + +/** + Pag纹理渲染类 + */ +@interface TGFlutterPagRender : NSObject + +///当前pag的size +@property(nonatomic, readonly) CGSize size; + +- (instancetype)initWithPagData:(NSData*)pagData + progress:(double)initProgress + frameUpdateCallback:(FrameUpdateCallback)frameUpdateCallback + eventCallback:(PAGEventCallback)eventCallback; + +- (void)startRender; + +- (void)stopRender; + +- (void)pauseRender; + +- (void)releaseRender; + +- (void)setProgress:(double)progress; + +- (void)setRepeatCount:(int)repeatCount; + +- (NSArray *)getLayersUnderPoint:(CGPoint)point; + +@end + +NS_ASSUME_NONNULL_END diff --git a/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagRender.mm b/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagRender.mm new file mode 100644 index 0000000..35fc9aa --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ios/Classes/TGFlutterPagRender.mm @@ -0,0 +1,186 @@ +// +// TGFlutterPageRender.m +// Tgclub +// +// Created by 黎敬茂 on 2021/11/25. +// Copyright © 2021 Tencent. All rights reserved. +// + +#import "TGFlutterPagRender.h" +#import +#import +#import +#import +#import +#include +#include +#include + +@interface TGFlutterPagRender() + +@property(nonatomic, strong)PAGSurface *surface; + +@property(nonatomic, strong)PAGPlayer* player; + +@property(nonatomic, strong)PAGFile* pagFile; + +@property(nonatomic, assign)double initProgress; + +@property(nonatomic, assign)BOOL endEvent; + + +@end + +static int64_t GetCurrentTimeUS() { + static auto START_TIME = std::chrono::high_resolution_clock::now(); + auto now = std::chrono::high_resolution_clock::now(); + auto ns = std::chrono::duration_cast(now - START_TIME); + return static_cast(ns.count() * 1e-3); +} + +@implementation TGFlutterPagRender +{ + FrameUpdateCallback _frameUpdateCallback; + PAGEventCallback _eventCallback; + CADisplayLink *_displayLink; + int _lastUpdateTs; + int _repeatCount; + int64_t start; + int64_t _currRepeatCount; +} + +- (CVPixelBufferRef)copyPixelBuffer { + + int64_t duration = [_player duration]; + if(duration <= 0){ + duration = 1; + } + + int64_t timestamp = GetCurrentTimeUS(); + if(start <= 0){ + start = timestamp; + } + auto count = (timestamp - start) / duration; + double value = 0; + if (_repeatCount >= 0 && count >= _repeatCount) { + value = 1; + if(!_endEvent){ + _endEvent = YES; + _eventCallback(EventEnd); + } + } else { + _endEvent = NO; + double playTime = (timestamp - start) % duration; + value = static_cast(playTime) / duration; + if (_currRepeatCount < count) { + _currRepeatCount = count; + _eventCallback(EventRepeat); + } + } + [_player setProgress:value]; + [_player flush]; + CVPixelBufferRef target = [_surface getCVPixelBuffer]; + CVBufferRetain(target); + return target; +} + +- (instancetype)initWithPagData:(NSData*)pagData + progress:(double)initProgress + frameUpdateCallback:(FrameUpdateCallback)frameUpdateCallback + eventCallback:(PAGEventCallback)eventCallback +{ + if (self = [super init]) { + _frameUpdateCallback = frameUpdateCallback; + _eventCallback = eventCallback; + _initProgress = initProgress; + if(pagData){ + _pagFile = [PAGFile Load:pagData.bytes size:pagData.length]; + _player = [[PAGPlayer alloc] init]; + [_player setComposition:_pagFile]; + _surface = [PAGSurface MakeFromGPU:CGSizeMake(_pagFile.width, _pagFile.height)]; + [_player setSurface:_surface]; + [_player setProgress:initProgress]; + [_player flush]; + _frameUpdateCallback(); + } + } + return self; +} + +- (void)startRender +{ + if (!_displayLink) { + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } + if(start <= 0){ + start = GetCurrentTimeUS(); + } + _eventCallback(EventStart); +} + +- (void)stopRender +{ + if (_displayLink) { + [_displayLink invalidate]; + _displayLink = nil; + } + [_player setProgress:_initProgress]; + [_player flush]; + _frameUpdateCallback(); + if(!_endEvent){ + _endEvent = YES; + _eventCallback(EventEnd); + } + _eventCallback(EventCancel); +} + +- (void)pauseRender{ + if (_displayLink) { + [_displayLink invalidate]; + _displayLink = nil; + } +} +- (void)setRepeatCount:(int)repeatCount{ + _repeatCount = repeatCount; +} + +- (void)setProgress:(double)progress{ + [_player setProgress:progress]; + [_player flush]; + _frameUpdateCallback(); +} + +- (NSArray *)getLayersUnderPoint:(CGPoint)point{ + NSArray* layers = [_player getLayersUnderPoint:point]; + NSMutableArray *layerNames = [[NSMutableArray alloc] init]; + for (PAGLayer *layer in layers) { + [layerNames addObject:layer.layerName]; + } + return layerNames; +} + +- (CGSize)size{ + return CGSizeMake(_pagFile.width, _pagFile.height); +} + +- (void)update +{ + _frameUpdateCallback(); +} + +- (void)releaseRender{ + if (_displayLink) { + [_displayLink invalidate]; + _displayLink = nil; + } +} + +- (void)dealloc { + _frameUpdateCallback = nil; + _eventCallback = nil; + _surface = nil; + self.pagFile = nil; + self.player = nil; +} +@end diff --git a/local_packages/pag-1.0.7-patched/ios/pag.podspec b/local_packages/pag-1.0.7-patched/ios/pag.podspec new file mode 100644 index 0000000..fc20772 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ios/pag.podspec @@ -0,0 +1,26 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_pag_plugin.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'pag' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin.' + s.description = <<-DESC +A new Flutter plugin. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.static_framework = true + s.dependency 'Flutter' + s.dependency 'libpag' + s.platform = :ios, '9.0' + s.library = 'c++' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } +end diff --git a/local_packages/pag-1.0.7-patched/lib/pag.dart b/local_packages/pag-1.0.7-patched/lib/pag.dart new file mode 100644 index 0000000..2588a53 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/lib/pag.dart @@ -0,0 +1,300 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class PAGView extends StatefulWidget { + /// 宽高,不建议不设置 + final double? width; + final double? height; + + /// 二进制动画数据 + final Uint8List? bytesData; + + /// 网络资源,动画链接 + final String? url; + + /// flutter动画资源路径 + final String? assetName; + + /// asset package + final String? package; + + /// 初始化时播放进度 + final double initProgress; + + /// 初始化后自动播放 + final bool autoPlay; + + /// 循环次数 + final int repeatCount; + + /// 初始化完成 + final PAGCallback? onInit; + + /// Notifies the start of the animation. + final PAGCallback? onAnimationStart; + + /// Notifies the end of the animation. + final PAGCallback? onAnimationEnd; + + /// Notifies the cancellation of the animation. + final PAGCallback? onAnimationCancel; + + /// Notifies the repetition of the animation. + final PAGCallback? onAnimationRepeat; + + /// 加载失败时的默认控件构造器 + final Widget Function(BuildContext context)? defaultBuilder; + + static const int REPEAT_COUNT_LOOP = -1; //无限循环 + static const int REPEAT_COUNT_DEFAULT = 1; //默认仅播放一次 + + const PAGView.network( + this.url, { + this.width, + this.height, + this.repeatCount = REPEAT_COUNT_DEFAULT, + this.initProgress = 0, + this.autoPlay = false, + this.onInit, + this.onAnimationStart, + this.onAnimationEnd, + this.onAnimationCancel, + this.onAnimationRepeat, + this.defaultBuilder, + Key? key, + }) : this.bytesData = null, + this.assetName = null, + this.package = null, + super(key: key); + + const PAGView.asset( + this.assetName, { + this.width, + this.height, + this.repeatCount = REPEAT_COUNT_DEFAULT, + this.initProgress = 0, + this.autoPlay = false, + this.package, + this.onInit, + this.onAnimationStart, + this.onAnimationEnd, + this.onAnimationCancel, + this.onAnimationRepeat, + this.defaultBuilder, + Key? key, + }) : this.bytesData = null, + this.url = null, + super(key: key); + + const PAGView.bytes( + this.bytesData, { + this.width, + this.height, + this.repeatCount = REPEAT_COUNT_DEFAULT, + this.initProgress = 0, + this.autoPlay = false, + this.package, + this.onInit, + this.onAnimationStart, + this.onAnimationEnd, + this.onAnimationCancel, + this.onAnimationRepeat, + this.defaultBuilder, + Key? key, + }) : this.url = null, + this.assetName = null, + super(key: key); + + @override + PAGViewState createState() => PAGViewState(); +} + +class PAGViewState extends State { + bool _hasLoadTexture = false; + int _textureId = -1; + + double rawWidth = 0; + double rawHeight = 0; + + static const double defaultSize = 50; + + // 原生接口 + static const String _nativeInit = 'initPag'; + static const String _nativeRelease = 'release'; + static const String _nativeStart = 'start'; + static const String _nativeStop = 'stop'; + static const String _nativePause = 'pause'; + static const String _nativeSetProgress = 'setProgress'; + static const String _nativeGetPointLayer = 'getLayersUnderPoint'; + static const String _nativeEnableCache = "enableCache"; + static const String _nativeSetCacheSize = "setCacheSize"; + static const String _nativeEnableMultiThread = "enableMultiThread"; + + // 参数 + static const String _argumentTextureId = 'textureId'; + static const String _argumentAssetName = 'assetName'; + static const String _argumentPackage = 'package'; + static const String _argumentUrl = 'url'; + static const String _argumentBytes = 'bytesData'; + static const String _argumentRepeatCount = 'repeatCount'; + static const String _argumentInitProgress = 'initProgress'; + static const String _argumentAutoPlay = 'autoPlay'; + static const String _argumentWidth = 'width'; + static const String _argumentHeight = 'height'; + static const String _argumentPointX = 'x'; + static const String _argumentPointY = 'y'; + static const String _argumentProgress = 'progress'; + static const String _argumentEvent = 'PAGEvent'; + static const String _argumentCacheEnabled = "cacheEnabled"; + static const String _argumentCacheSize = "cacheSize"; + static const String _argumentMultiThreadEnabled = "multiThreadEnabled"; + + // 监听该函数 + static const String _playCallback = 'PAGCallback'; + static const String _eventStart = 'onAnimationStart'; + static const String _eventEnd = 'onAnimationEnd'; + static const String _eventCancel = 'onAnimationCancel'; + static const String _eventRepeat = 'onAnimationRepeat'; + static const String _eventUpdate = 'onAnimationUpdate'; + + // 回调监听 + static MethodChannel _channel = (const MethodChannel('flutter_pag_plugin') + ..setMethodCallHandler((result) { + if (result.method == _playCallback) { + callbackHandlers[result.arguments[_argumentTextureId]]?.call(result.arguments[_argumentEvent]); + } + + return Future.value(); + })); + + static Map callbackHandlers = {}; + + @override + void initState() { + super.initState(); + newTexture(); + } + + // 初始化 + void newTexture() async { + int repeatCount = widget.repeatCount <= 0 && widget.repeatCount != PAGView.REPEAT_COUNT_LOOP ? PAGView.REPEAT_COUNT_DEFAULT : widget.repeatCount; + double initProcess = widget.initProgress < 0 ? 0 : widget.initProgress; + + try { + dynamic result = + await _channel.invokeMethod(_nativeInit, {_argumentAssetName: widget.assetName, _argumentPackage: widget.package, _argumentUrl: widget.url, _argumentBytes: widget.bytesData, _argumentRepeatCount: repeatCount, _argumentInitProgress: initProcess, _argumentAutoPlay: widget.autoPlay}); + if (result is Map) { + _textureId = result[_argumentTextureId]; + rawWidth = result[_argumentWidth] ?? 0; + rawHeight = result[_argumentHeight] ?? 0; + } + if (mounted) { + setState(() { + _hasLoadTexture = true; + }); + widget.onInit?.call(); + } else { + _channel.invokeMethod(_nativeRelease, {_argumentTextureId: _textureId}); + } + } catch (e) { + print('PAGViewState error: $e'); + } + + // 事件回调 + if (_textureId >= 0) { + var events = { + _eventStart: widget.onAnimationStart, + _eventEnd: widget.onAnimationEnd, + _eventCancel: widget.onAnimationCancel, + _eventRepeat: widget.onAnimationRepeat, + }; + callbackHandlers[_textureId] = (event) { + events[event]?.call(); + }; + } + } + + /// 开始 + void start() { + if (!_hasLoadTexture) { + return; + } + _channel.invokeMethod(_nativeStart, {_argumentTextureId: _textureId}); + } + + /// 停止 + void stop() { + if (!_hasLoadTexture) { + return; + } + _channel.invokeMethod(_nativeStop, {_argumentTextureId: _textureId}); + } + + /// 暂停 + void pause() { + if (!_hasLoadTexture) { + return; + } + _channel.invokeMethod(_nativePause, {_argumentTextureId: _textureId}); + } + + /// 设置进度 + void setProgress(double progress) { + if (!_hasLoadTexture) { + return; + } + _channel.invokeMethod(_nativeSetProgress, {_argumentTextureId: _textureId, _argumentProgress: progress}); + } + + /// 获取某一位置的图层 + Future> getLayersUnderPoint(double x, double y) async { + if (!_hasLoadTexture) { + return []; + } + return (await _channel.invokeMethod(_nativeGetPointLayer, {_argumentTextureId: _textureId, _argumentPointX: x, _argumentPointY: y}) as List).map((e) => e.toString()).toList(); + } + + @override + Widget build(BuildContext context) { + if (_hasLoadTexture) { + return SizedBox( + width: widget.width ?? (rawWidth / 2), + height: widget.height ?? (rawHeight / 2), + child: Texture(textureId: _textureId), + ); + } else { + return widget.defaultBuilder?.call(context) ?? + SizedBox( + width: widget.width ?? defaultSize, + height: widget.height ?? defaultSize, + ); + } + } + + @override + void dispose() { + super.dispose(); + _channel.invokeMethod(_nativeRelease, {_argumentTextureId: _textureId}); + callbackHandlers.remove(_textureId); + } +} + +typedef PAGCallback = void Function(); + +// PAG设置 +class PAG { + // 是否开启缓存,默认true + static void enableCache(bool enable) { + PAGViewState._channel.invokeMethod(PAGViewState._nativeEnableCache, {PAGViewState._argumentCacheEnabled: enable}); + } + + // 是否多线程加载和释放资源,默认true + static void enableMultiThread(bool enable) { + PAGViewState._channel.invokeMethod(PAGViewState._nativeEnableMultiThread, {PAGViewState._argumentMultiThreadEnabled: enable}); + } + + // 设置缓存数量,默认10 + static void setCacheSize(int size) { + PAGViewState._channel.invokeMethod(PAGViewState._nativeSetCacheSize, {PAGViewState._argumentCacheSize: size}); + } +} diff --git a/local_packages/pag-1.0.7-patched/ohos/build-profile.json5 b/local_packages/pag-1.0.7-patched/ohos/build-profile.json5 new file mode 100644 index 0000000..265367c --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ohos/build-profile.json5 @@ -0,0 +1,25 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + }, + }, + ], + "targets": [ + { + "name": "default" + } + ] +} diff --git a/local_packages/pag-1.0.7-patched/ohos/hvigorfile.ts b/local_packages/pag-1.0.7-patched/ohos/hvigorfile.ts new file mode 100644 index 0000000..47e6e1f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ohos/hvigorfile.ts @@ -0,0 +1,2 @@ +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +export { harTasks } from '@ohos/hvigor-ohos-plugin'; \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/ohos/index.ets b/local_packages/pag-1.0.7-patched/ohos/index.ets new file mode 100644 index 0000000..582b23f --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ohos/index.ets @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import FlutterPagPlugin from './src/main/ets/components/plugin/FlutterPagPlugin'; +export default FlutterPagPlugin; diff --git a/local_packages/pag-1.0.7-patched/ohos/oh-package.json5 b/local_packages/pag-1.0.7-patched/ohos/oh-package.json5 new file mode 100644 index 0000000..244058e --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ohos/oh-package.json5 @@ -0,0 +1,12 @@ +{ + "name": "pag", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "index.ets", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@ohos/flutter_ohos": "file:./har/flutter.har", + "@tencent/libpag": "^4.4.21", + } +} diff --git a/local_packages/pag-1.0.7-patched/ohos/src/main/ets/components/plugin/FlutterPagPlayer.ets b/local_packages/pag-1.0.7-patched/ohos/src/main/ets/components/plugin/FlutterPagPlayer.ets new file mode 100644 index 0000000..60c709d --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ohos/src/main/ets/components/plugin/FlutterPagPlayer.ets @@ -0,0 +1,85 @@ +import { PAGPlayer } from '@tencent/libpag/src/main/ets/PAGPlayer'; + +import { AnimatorOptions, AnimatorResult, Animator } from '@kit.ArkUI'; + +import { Log } from '@ohos/flutter_ohos'; + +// AnimationCallback: () => void +type AnimationCallback = Callback; +const DefaultCallback: AnimationCallback = () => { +}; + +const TAG = "FlutterPagPlayer"; + +export class FlutterPagPlayer extends PAGPlayer { + private animator: AnimatorResult | undefined = undefined; + private onStartCallback: AnimationCallback | undefined = undefined; + + constructor() { + super(); + } + + init(repeatCount: number = -1, + onStart: AnimationCallback = DefaultCallback, + onEnd: AnimationCallback = DefaultCallback, + onCancel: AnimationCallback = DefaultCallback, + onRepeat: AnimationCallback = DefaultCallback, + onUpdate: AnimationCallback = DefaultCallback) { + let animatorOption: AnimatorOptions = { + duration: this.duration() / 1000, + delay: 0, + easing: 'linear', + iterations: repeatCount, + fill: "forwards", + direction: 'normal', + begin: this.getProgress(), + end: 1 + }; + + this.animator = Animator.create(animatorOption); + this.animator.onFrame = (progress: number) => { + // Log.d(FlutterPagPlayer.TAG, "animator onFrame: duration: %d; progress: %d; maxFrameRate:%d; currentFrame: %d", + // this.duration(), progress, this.maxFrameRate(), this.currentFrame()); + this.setProgress(progress); + this.flush(); + }; + this.onStartCallback = onStart; + this.animator.onRepeat = () => { + // Log.d(TAG, "animator onRepeat"); + onRepeat(); + }; + this.animator.onCancel = () => { + Log.i(TAG, "animator onCancel"); + onCancel(); + }; + this.animator.onFinish = () => { + Log.i(TAG, "animator onFinish"); + onEnd(); + }; + } + + release() { + Log.i(TAG, "release"); + this.animator?.cancel(); + this.animator = undefined; + this.onStartCallback = undefined; + this.getSurface()?.release(); + super.release(); + } + + start() { + Log.i(TAG, "start"); + this.animator?.play(); + this.onStartCallback?.(); + } + + stop() { + Log.i(TAG, "stop"); + this.animator?.cancel(); + } + + pause() { + Log.i(TAG, "pause"); + this.animator?.pause(); + } +} \ No newline at end of file diff --git a/local_packages/pag-1.0.7-patched/ohos/src/main/ets/components/plugin/FlutterPagPlugin.ets b/local_packages/pag-1.0.7-patched/ohos/src/main/ets/components/plugin/FlutterPagPlugin.ets new file mode 100644 index 0000000..8e6f232 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ohos/src/main/ets/components/plugin/FlutterPagPlugin.ets @@ -0,0 +1,346 @@ +import { + FlutterPlugin, + FlutterPluginBinding, + Log, + MethodCall, + MethodCallHandler, + MethodChannel, + MethodResult, + SurfaceTextureEntry, +} from '@ohos/flutter_ohos'; + +import hilog from '@ohos.hilog'; +import { PAGFile, PAGSurface } from '@tencent/libpag' +import { FlutterPagPlayer } from './FlutterPagPlayer'; + +namespace Constants { + // 原生接口 + export const _nativeInit = "initPag"; + + export const _nativeRelease = "release"; + + export const _nativeStart = "start"; + + export const _nativeStop = "stop"; + + export const _nativePause = "pause"; + + export const _nativeSetProgress = "setProgress"; + + export const _nativeGetPointLayer = "getLayersUnderPoint"; + + // 参数 + export const _argumentTextureId = "textureId"; + + export const _argumentAssetName = "assetName"; + + export const _argumentPackage = "package"; + + export const _argumentUrl = "url"; + + export const _argumentBytes = "bytesData"; + + export const _argumentRepeatCount = "repeatCount"; + + export const _argumentInitProgress = "initProgress"; + + export const _argumentAutoPlay = "autoPlay"; + + export const _argumentWidth = "width"; + + export const _argumentHeight = "height"; + + export const _argumentPointX = "x"; + + export const _argumentPointY = "y"; + + export const _argumentProgress = "progress"; + + export const _argumentEvent = "PAGEvent"; + + // 回调 + export const _playCallback = "PAGCallback"; + + export const _eventStart = "onAnimationStart"; + + export const _eventEnd = "onAnimationEnd"; + + export const _eventCancel = "onAnimationCancel"; + + export const _eventRepeat = "onAnimationRepeat"; + + export const _eventUpdate = "onAnimationUpdate"; +} + +const TAG = "FlutterPagPlugin"; + +/** FlutterPagPlugin **/ +export default class FlutterPagPlugin implements FlutterPlugin, MethodCallHandler { + private channel: MethodChannel | null = null; + private flutterBinding: FlutterPluginBinding | null = null; + private playerMap: Map = new Map(); + private entryMap: Map = new Map(); + + constructor() { + Log.setLogLevel(hilog.LogLevel.INFO); + } + + getUniqueClassName(): string { + return "FlutterPagPlugin" + } + + onAttachedToEngine(binding: FlutterPluginBinding): void { + Log.i(TAG, "onAttachedToEngine"); + + this.flutterBinding = binding; + this.channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_pag_plugin"); + this.channel.setMethodCallHandler(this) + } + + onDetachedFromEngine(binding: FlutterPluginBinding): void { + Log.i(TAG, "onDetachedFromEngine"); + + this.channel?.setMethodCallHandler(null); + this.channel = null; + this.flutterBinding = null; + + this.playerMap.forEach((player, _) => { + player.stop(); + player.release(); + }); + this.playerMap.clear(); + + this.entryMap.forEach((_, textureId) => { + this.flutterBinding?.getTextureRegistry()?.unregisterTexture(textureId) + }); + this.entryMap.clear(); + } + + onMethodCall(call: MethodCall, result: MethodResult): void { + Log.i(TAG, `onMethodCall => ${call.method}`); + + switch (call.method) { + case Constants._nativeInit: + this.initPag(call, result); + break; + case Constants._nativeRelease: + this.release(call); + result.success(null); + break; + case Constants._nativeStart: + this.start(call); + result.success(null); + break; + case Constants._nativeStop: + this.stop(call); + result.success(null); + break; + case Constants._nativePause: + this.pause(call); + result.success(null); + break; + case Constants._nativeSetProgress: + this.setProgress(call); + result.success(null); + break; + case Constants._nativeGetPointLayer: + let layerNames = this.getLayersUnderPoint(call); + result.success(layerNames); + break; + default: + result.notImplemented(); + break; + } + } + + private initPag(call: MethodCall, result: MethodResult) { + let assetName: string | undefined = call.argument(Constants._argumentAssetName); + let bytes: Uint8Array | undefined = call.argument(Constants._argumentBytes); + let url: string | undefined = call.argument(Constants._argumentUrl); + let flutterPackage: string | undefined = call.argument(Constants._argumentPackage); + + Log.d(TAG, + `initPag assetName: ${assetName}; bytes: ${bytes?.length}; url: ${url}; flutterPackage: ${flutterPackage}`); + + if (assetName != undefined) { + let manager = this.flutterBinding!.getApplicationContext().resourceManager; + let assetPath = this.flutterBinding?.getFlutterAssets().getAssetFilePathByName(assetName, flutterPackage ?? ''); + Log.i(TAG, `initPag LoadFromAssets assetName=${assetName}; path: ${assetPath}`); + let pagFile = PAGFile.LoadFromAssets(manager, assetPath!); + this.initPagPlayer(call, result, pagFile); + } else if (bytes != undefined) { + Log.i(TAG, `initPag LoadFromBytes bytes ByteLength: ${bytes.byteLength};`); + let pagFile = PAGFile.LoadFromBytes(bytes.buffer as ArrayBuffer); + this.initPagPlayer(call, result, pagFile); + } else if (url != undefined) { + Log.i(TAG, `initPag LoadFromPathAsync url: ${url}`); + PAGFile.LoadFromPathAsync(url).then((pagFile) => { + this.initPagPlayer(call, result, pagFile); + }).catch(() => { + Log.e(TAG, `initPag LoadFromPathAsync error: ${url}`); + result.error("500", "LoadFromPathAsync error", null); + }); + } else { + result.error("400", "Invalid arguments", null); + return; + } + } + + private initPagPlayer(call: MethodCall, result: MethodResult, pagFile: PAGFile | null) { + if (pagFile == null) { + Log.e(TAG, "initPagPlayer pagFile is null..."); + result.error("400", "initPagPlayer pagFile is null", null); + return; + } + + try { + let textureRegistry = this.flutterBinding?.getTextureRegistry(); + let textureId: number = textureRegistry!.getTextureId(); + let surfaceTextureEntry = textureRegistry!.registerTexture(textureId); + let surfaceId: number = surfaceTextureEntry.getSurfaceId(); + Log.i(TAG, `initPagPlayer textureId:${textureId}; surfaceId: ${surfaceId}`); + + let width = pagFile?.width(); + let height = pagFile?.height(); + Log.i(TAG, `initPagPlayer pagFile width: ${width}; height: ${height}`); + + textureRegistry?.setTextureBufferSize(textureId, width, height); + + let pagSurface = PAGSurface.FromSurfaceID(surfaceId); + Log.i(TAG, + `initPagPlayer pagSurface:${pagSurface}; width:${pagSurface?.width()}; height: ${pagSurface?.height()}`); + + let repeatCount: number = call.argument(Constants._argumentRepeatCount) ?? -1; + let initProgress: number = call.argument(Constants._argumentInitProgress) ?? 0; + let autoPlay: boolean = call.argument(Constants._argumentAutoPlay) ?? true; + pagFile.setProgress(initProgress); + + let pagPlayer = new FlutterPagPlayer(); + pagPlayer.setComposition(pagFile); + pagPlayer.setSurface(pagSurface); + pagPlayer.setProgress(initProgress); + pagPlayer.init(repeatCount, () => { + this.notifyEvent(Constants._eventStart, textureId); + }, () => { + this.notifyEvent(Constants._eventEnd, textureId); + }, () => { + this.notifyEvent(Constants._eventCancel, textureId); + }, () => { + this.notifyEvent(Constants._eventRepeat, textureId); + }); + + let callback = new Map(); + callback.set(Constants._argumentTextureId, textureId); + // TODO: ets number convert to dart `double` type have some problem, add 00001 to avoid convert to `int` + // See this issues https://gitee.com/openharmony-sig/flutter_flutter/issues/I9QR10 + callback.set(Constants._argumentWidth, width + 0.00001); + callback.set(Constants._argumentHeight, height + 0.00001); + + this.playerMap.set(textureId, pagPlayer); + this.entryMap.set(textureId, surfaceTextureEntry); + + // TODO: Post to next frame start to avoid crash, otherwise 【Release mode】 will crash. + // See this issues https://gitee.com/openharmony-sig/flutter_engine/issues/IAYIEG + setTimeout(() => { + pagPlayer.prepare(); + pagPlayer.flush(); + + // auto play + if (autoPlay) { + pagPlayer.start(); + } + result.success(callback); + }, 0); + } catch (e) { + Log.e(TAG, `initPagPlayer error: ${e}`); + result.error("500", "initPagPlayer error", null); + } + } + + private release(call: MethodCall) { + Log.i(TAG, "release"); + + let textureId: number = call.argument(Constants._argumentTextureId) as number; + let pagPlayer = this.getFlutterPagPlayer(call); + if (pagPlayer != null) { + pagPlayer.stop(); + pagPlayer.release(); + this.playerMap.delete(textureId); + } + + let surfaceTextureEntry = this.entryMap.get(textureId); + if (surfaceTextureEntry != null) { + // TODO: ohos not implemented `release` method now + // surfaceTextureEntry.release(); + this.entryMap.delete(textureId); + } + this.flutterBinding?.getTextureRegistry()?.unregisterTexture(textureId); + } + + private start(call: MethodCall) { + Log.i(TAG, "start"); + + let pagPlayer = this.getFlutterPagPlayer(call); + if (pagPlayer != null) { + pagPlayer.start(); + } + } + + private stop(call: MethodCall) { + Log.i(TAG, "stop"); + + let pagPlayer = this.getFlutterPagPlayer(call); + if (pagPlayer != null) { + pagPlayer.stop(); + } + } + + private pause(call: MethodCall) { + Log.i(TAG, "pause"); + + let pagPlayer = this.getFlutterPagPlayer(call); + if (pagPlayer != null) { + pagPlayer.pause(); + } + } + + private setProgress(call: MethodCall) { + Log.i(TAG, "setProgress"); + + let pagPlayer = this.getFlutterPagPlayer(call); + if (pagPlayer != null) { + let progress = call.argument(Constants._argumentProgress) as number; + pagPlayer.setProgress(progress); + } + } + + private getLayersUnderPoint(call: MethodCall): Array { + Log.i(TAG, "getLayersUnderPoint"); + + let layerNames: Array = new Array(); + let pagPlayer = this.getFlutterPagPlayer(call); + if (pagPlayer != null) { + let x = call.argument(Constants._argumentPointX) as number; + let y = call.argument(Constants._argumentPointY) as number; + let layers = pagPlayer.getLayersUnderPoint(x, y); + layers.forEach((layer) => { + layerNames.push(layer.layerName()); + }); + } + return layerNames; + } + + private notifyEvent(event: string, textureId: number) { + let callback = new Map(); + callback.set(Constants._argumentTextureId, textureId); + callback.set(Constants._argumentEvent, event); + + this.channel?.invokeMethod(Constants._playCallback, callback); + } + + private getFlutterPagPlayer(call: MethodCall): FlutterPagPlayer | undefined { + let textureId = call.argument(Constants._argumentTextureId) as number; + return this.playerMap.get(textureId); + } +} + diff --git a/local_packages/pag-1.0.7-patched/ohos/src/main/module.json5 b/local_packages/pag-1.0.7-patched/ohos/src/main/module.json5 new file mode 100644 index 0000000..a46de29 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/ohos/src/main/module.json5 @@ -0,0 +1,10 @@ +{ + "module": { + "name": "pag", + "type": "har", + "deviceTypes": [ + "default", + "tablet" + ] + } +} diff --git a/local_packages/pag-1.0.7-patched/pubspec.yaml b/local_packages/pag-1.0.7-patched/pubspec.yaml new file mode 100644 index 0000000..aaf8ca7 --- /dev/null +++ b/local_packages/pag-1.0.7-patched/pubspec.yaml @@ -0,0 +1,67 @@ +name: pag +description: PAG for flutter +version: 1.0.7 +homepage: https://github.com/libpag/pag-flutter + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.example.flutter_pag_plugin + pluginClass: FlutterPagPlugin + ios: + pluginClass: FlutterPagPlugin + ohos: + package: com.tencent.flutter_pag_plugin + pluginClass: FlutterPagPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/pubspec.lock b/pubspec.lock index c1e1601..39f96ee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,14 +9,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.59" - agora_rtc_engine: - dependency: "direct main" - description: - name: agora_rtc_engine - sha256: "6559294d18ce4445420e19dbdba10fb58cac955cd8f22dbceae26716e194d70e" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.5.3" app_links: dependency: "direct main" description: @@ -915,14 +907,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.20.2" - iris_method_channel: - dependency: transitive - description: - name: iris_method_channel - sha256: "114bbe541369add8dd0727858e7df5764f375e3fb88374ad487301733fddb57f" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.5" isolate_easy_pool: dependency: "direct main" description: @@ -1148,10 +1132,9 @@ packages: pag: dependency: "direct main" description: - name: pag - sha256: "8ba3b9c18309b0ef71ad53d20c67ed13893ad3cc49fdb7cc48a8d4e78bfe6db7" - url: "https://pub.flutter-io.cn" - source: hosted + path: "local_packages/pag-1.0.7-patched" + relative: true + source: path version: "1.0.7" path: dependency: "direct main" @@ -1557,6 +1540,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "8.3.6498+3" + tencent_rtc_sdk: + dependency: "direct main" + description: + name: tencent_rtc_sdk + sha256: "928b91ff2642b7e254aba2f4228b8c273aae34357fe4a74e7172aabf604d3df5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "13.2.2" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2c4a4ac..ec37045 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,9 +47,8 @@ dependencies: pretty_dio_logger: ^1.1.1 #下拉刷新 pull_to_refresh: 2.0.0 - #声网 - agora_rtc_engine: ^6.5.2 - # agora_rtm: 1.6.0 + #腾讯TRTC + tencent_rtc_sdk: ^13.2.2 #腾讯IM tencent_cloud_chat_sdk: 8.3.6498+3 #缓存 @@ -134,7 +133,8 @@ dependencies: #外链跳转 app_links: ^6.4.1 flutter_svg: ^2.3.0 - pag: ^1.0.7 + pag: + path: ./local_packages/pag-1.0.7-patched dev_dependencies: flutter_launcher_icons: ^0.14.4 diff --git a/test/room_rocket_status_test.dart b/test/room_rocket_status_test.dart new file mode 100644 index 0000000..ed212af --- /dev/null +++ b/test/room_rocket_status_test.dart @@ -0,0 +1,140 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; +import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart'; + +void main() { + test( + 'parses level-specific reward previews and rocket kings from levels', + () { + final status = SCRoomRocketStatusRes.fromJson({ + 'currentLevel': 1, + 'levels': [ + { + 'level': 1, + 'rewardPreview': { + 'top1': [ + { + 'rewardScene': 'TOP1', + 'rewardType': 'GOLD', + 'rewardAmount': 100, + }, + ], + }, + 'rocketKings': [ + {'rank': 1, 'userId': 'u1', 'userAvatar': 'avatar-1'}, + ], + }, + { + 'level': 2, + 'rewardPreview': { + 'ignite': [ + { + 'rewardScene': 'IGNITE', + 'rewardType': 'GOLD', + 'rewardAmount': 200, + }, + ], + }, + 'rocketKings': [ + {'rank': 1, 'userId': 'u2', 'userAvatar': 'avatar-2'}, + {'rank': 2, 'userId': 'u3', 'userAvatar': 'avatar-3'}, + {'rank': 3, 'userId': 'u4', 'userAvatar': 'avatar-4'}, + ], + }, + ], + }); + + expect(status.rewardPreviewForLevel(1)?.top1.single.rewardAmount, 100); + expect(status.rewardPreviewForLevel(2)?.ignite.single.rewardAmount, 200); + expect(status.rocketKingsForLevel(1).single.userAvatar, 'avatar-1'); + expect(status.rocketKingsForLevel(2), hasLength(3)); + }, + ); + + test('parses level-specific reward previews and rocket kings from maps', () { + final status = SCRoomRocketStatusRes.fromJson({ + 'currentLevel': 1, + 'rewardPreview': { + '1': { + 'top1': [ + {'rewardScene': 'TOP1', 'rewardType': 'GOLD', 'rewardAmount': 100}, + ], + }, + '2': { + 'inRoom': [ + { + 'rewardScene': 'IN_ROOM', + 'rewardType': 'GOLD', + 'rewardAmount': 300, + }, + ], + }, + }, + 'rocketKings': { + '1': [ + {'rank': 1, 'userId': 'u1', 'userAvatar': 'avatar-1'}, + ], + '2': [ + {'rank': 1, 'userId': 'u2', 'userAvatar': 'avatar-2'}, + {'rank': 2, 'userId': 'u3', 'userAvatar': 'avatar-3'}, + {'rank': 3, 'userId': 'u4', 'userAvatar': 'avatar-4'}, + ], + }, + }); + + expect(status.rewardPreviewForLevel(1)?.top1.single.rewardAmount, 100); + expect(status.rewardPreviewForLevel(2)?.inRoom.single.rewardAmount, 300); + expect(status.rocketKingsForLevel(1).single.userAvatar, 'avatar-1'); + expect(status.rocketKingsForLevel(2), hasLength(3)); + }); + + test( + 'uses top-level reward preview for the response level instead of current level', + () { + final status = SCRoomRocketStatusRes.fromJson({ + 'currentLevel': 1, + 'level': 2, + 'rewardPreview': { + 'top1': [ + {'rewardScene': 'TOP1', 'rewardType': 'GOLD', 'rewardAmount': 200}, + ], + }, + }); + + expect(status.rewardPreviewForLevel(1), isNull); + expect(status.rewardPreviewForLevel(2)?.top1.single.rewardAmount, 200); + }, + ); + + test('maps enabled rocket configs into level-specific reward groups', () { + final groups = RoomRocketApiMapper.rewardGroupsByLevelFromConfigs([ + SCRoomRocketConfigRes.fromJson({ + 'level': 1, + 'rewardConfig': [ + {'type': 'GOLD', 'detailType': 'GOLD', 'amount': 100, 'sort': 1}, + ], + }), + SCRoomRocketConfigRes.fromJson({ + 'level': 2, + 'rewardConfig': [ + {'type': 'GOLD', 'detailType': 'GOLD', 'amount': 200, 'sort': 1}, + ], + 'normalRewardConfig': [ + { + 'type': 'PROPS', + 'detailType': 'AVATAR_FRAME', + 'cover': 'cover-2', + 'quantity': 7, + 'sort': 2, + }, + ], + }), + ]); + + expect(groups[1]?.top1.single.amount, '100'); + expect(groups[2]?.top1.single.amount, '200'); + expect(groups[2]?.inRoom.single.imageUrl, 'cover-2'); + expect(groups[2]?.inRoom.single.expireDays, 7); + }); +} diff --git a/test/room_rtc_types_test.dart b/test/room_rtc_types_test.dart new file mode 100644 index 0000000..d69a169 --- /dev/null +++ b/test/room_rtc_types_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/services/audio/room_rtc_types.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; + +void main() { + test('RoomRtcSessionIdentity uses roomAccount and business userId', () { + final identity = RoomRtcSessionIdentity.fromRoom( + roomProfile: SocialChatRoomRes( + id: 'room-id', + roomAccount: 'room-account', + ), + userProfile: SocialChatUserProfile(id: 'user-id', account: '10001'), + ); + + expect(identity.businessRoomId, 'room-id'); + expect(identity.trtcStrRoomId, 'room-account'); + expect(identity.trtcUserId, 'user-id'); + expect(identity.isValid, isTrue); + }); + + test('RoomRtcSessionIdentity is invalid without strRoomId or userId', () { + final identity = RoomRtcSessionIdentity.fromRoom( + roomProfile: SocialChatRoomRes(id: 'room-id', roomAccount: ''), + userProfile: SocialChatUserProfile(id: 'user-id'), + ); + + expect(identity.isValid, isFalse); + }); +} diff --git a/test/trtc_room_rtc_engine_adapter_test.dart b/test/trtc_room_rtc_engine_adapter_test.dart new file mode 100644 index 0000000..480a233 --- /dev/null +++ b/test/trtc_room_rtc_engine_adapter_test.dart @@ -0,0 +1,250 @@ +import 'package:flutter_test/flutter_test.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 'package:yumi/services/audio/room_rtc_engine_adapter.dart'; +import 'package:yumi/services/audio/room_rtc_types.dart'; +import 'package:yumi/services/audio/trtc_room_rtc_engine_adapter.dart'; + +void main() { + test('enters TRTC voice room with strRoomId and business userId', () async { + final cloud = _FakeTrtcCloud(); + final adapter = TrtcRoomRtcEngineAdapter(cloudFactory: () async => cloud); + + await adapter.enterRoomAsAudience( + const RoomRtcEnterRoomConfig( + identity: RoomRtcSessionIdentity( + businessRoomId: 'business-room', + trtcStrRoomId: 'room-account', + trtcUserId: 'user-id', + ), + trtcSdkAppId: 20036101, + trtcUserSig: 'sig', + throwOnError: true, + ), + ); + + expect(cloud.enteredParams?.sdkAppId, 20036101); + expect(cloud.enteredParams?.roomId, 0); + expect(cloud.enteredParams?.strRoomId, 'room-account'); + expect(cloud.enteredParams?.userId, 'user-id'); + expect(cloud.enteredParams?.userSig, 'sig'); + expect(cloud.enteredParams?.role, TRTCRoleType.audience); + expect(cloud.enteredScene, TRTCAppScene.voiceChatRoom); + expect(cloud.autoRecvAudio, isTrue); + expect(cloud.autoRecvVideo, isFalse); + expect(cloud.volumeEvaluationEnabled, isTrue); + }); + + test('switches between anchor and audience with local audio state', () async { + final cloud = _FakeTrtcCloud(); + final adapter = TrtcRoomRtcEngineAdapter(cloudFactory: () async => cloud); + + await _enter(adapter); + expect( + await adapter.switchToAnchor(const RoomRtcAnchorConfig(muted: true)), + isTrue, + ); + expect(cloud.lastRole, TRTCRoleType.anchor); + expect(cloud.localAudioStarted, isTrue); + expect(cloud.localAudioMuted, isTrue); + + expect(await adapter.switchToAudience(), isTrue); + expect(cloud.lastRole, TRTCRoleType.audience); + expect(cloud.localAudioStopped, isTrue); + expect(cloud.localAudioMuted, isTrue); + }); + + test('maps TRTC music calls to AudioEffectManager', () async { + final cloud = _FakeTrtcCloud(); + final adapter = TrtcRoomRtcEngineAdapter(cloudFactory: () async => cloud); + final states = []; + final musicAdapter = TrtcRoomRtcEngineAdapter( + cloudFactory: () async => cloud, + onAudioMixingStateChanged: (state, _) => states.add(state), + ); + + await _enter(adapter); + await _enter(musicAdapter); + await musicAdapter.startAudioMixing( + const RoomRtcAudioMixingConfig( + filePath: '/tmp/a.mp3', + loopback: false, + cycle: 3, + startPos: 1200, + ), + ); + + final musicParam = cloud.audioEffectManager.startedMusicParam!; + expect(musicParam.id, 1); + expect(musicParam.path, '/tmp/a.mp3'); + expect(musicParam.publish, isTrue); + expect(musicParam.loopCount, 2); + expect(musicParam.startTimeMS, 1200); + expect(states, contains(RoomRtcAudioMixingState.playing)); + + await musicAdapter.pauseAudioMixing(); + await musicAdapter.resumeAudioMixing(); + await musicAdapter.setAudioMixingVolume(80); + + expect(cloud.audioEffectManager.pausedIds, contains(1)); + expect(cloud.audioEffectManager.resumedIds, contains(1)); + expect(cloud.audioEffectManager.playoutVolume, 80); + expect(cloud.audioEffectManager.publishVolume, 80); + }); +} + +Future _enter(TrtcRoomRtcEngineAdapter adapter) { + return adapter.enterRoomAsAudience( + const RoomRtcEnterRoomConfig( + identity: RoomRtcSessionIdentity( + businessRoomId: 'business-room', + trtcStrRoomId: 'room-account', + trtcUserId: 'user-id', + ), + trtcSdkAppId: 20036101, + trtcUserSig: 'sig', + throwOnError: true, + ), + ); +} + +class _FakeTrtcCloud implements RoomTrtcCloud { + final _FakeAudioEffectManager audioEffectManager = _FakeAudioEffectManager(); + + TRTCCloudListener? listener; + TRTCParams? enteredParams; + TRTCAppScene? enteredScene; + TRTCRoleType? lastRole; + bool? autoRecvAudio; + bool? autoRecvVideo; + bool volumeEvaluationEnabled = false; + bool localAudioStarted = false; + bool localAudioStopped = false; + bool localAudioMuted = false; + + @override + void registerListener(TRTCCloudListener listener) { + this.listener = listener; + } + + @override + void unRegisterListener(TRTCCloudListener listener) { + if (this.listener == listener) { + this.listener = null; + } + } + + @override + void setDefaultStreamRecvMode(bool autoRecvAudio, bool autoRecvVideo) { + this.autoRecvAudio = autoRecvAudio; + this.autoRecvVideo = autoRecvVideo; + } + + @override + void enableAudioVolumeEvaluation( + bool enable, + TRTCAudioVolumeEvaluateParams params, + ) { + volumeEvaluationEnabled = enable; + } + + @override + void enterRoom(TRTCParams param, TRTCAppScene scene) { + enteredParams = param; + enteredScene = scene; + listener?.onEnterRoom?.call(10); + } + + @override + void exitRoom() { + listener?.onExitRoom?.call(0); + } + + @override + void switchRole(TRTCRoleType role) { + lastRole = role; + } + + @override + void startLocalAudio(TRTCAudioQuality quality) { + localAudioStarted = true; + } + + @override + void stopLocalAudio() { + localAudioStopped = true; + } + + @override + void muteLocalAudio(bool mute) { + localAudioMuted = mute; + } + + @override + void muteAllRemoteAudio(bool mute) {} + + @override + RoomTrtcAudioEffectManager getAudioEffectManager() { + return audioEffectManager; + } +} + +class _FakeAudioEffectManager implements RoomTrtcAudioEffectManager { + TXMusicPlayObserver? observer; + AudioMusicParam? startedMusicParam; + final pausedIds = []; + final resumedIds = []; + int? playoutVolume; + int? publishVolume; + int position = 0; + + @override + void setMusicObserver(int musicId, TXMusicPlayObserver? observer) { + this.observer = observer; + } + + @override + void startPlayMusic(AudioMusicParam musicParam) { + startedMusicParam = musicParam; + observer?.onStart(musicParam.id, 0); + } + + @override + void stopPlayMusic(int id) {} + + @override + void pausePlayMusic(int id) { + pausedIds.add(id); + } + + @override + void resumePlayMusic(int id) { + resumedIds.add(id); + } + + @override + void setMusicPublishVolume(int id, int volume) { + publishVolume = volume; + } + + @override + void setMusicPlayoutVolume(int id, int volume) { + playoutVolume = volume; + } + + @override + int getMusicCurrentPosInMS(int id) { + return position; + } + + @override + int getMusicDurationInMS(String path) { + return 3000; + } + + @override + void seekMusicToPosInTime(int id, int pts) { + position = pts; + } +} diff --git a/需求进度.md b/需求进度.md index ead1971..8412071 100644 --- a/需求进度.md +++ b/需求进度.md @@ -1,5 +1,21 @@ # CP 进度弹窗 Desktop MCP 复核进度 +- [x] 2026-05-15 本轮重新执行语音房 Agora -> TRTC 迁移:确认 Flutter RTC 口径改为 TRTC `strRoomId = roomProfile.roomAccount`、`userId = 业务 userId`,不再使用 Agora `roomProfile.id` 频道号和数字 uid。 +- [x] 2026-05-15 已移除 Flutter Agora 依赖并接入腾讯官方 `tencent_rtc_sdk: ^13.2.2`;`flutter pub get` 已刷新 `pubspec.lock` 和 `.flutter-plugins-dependencies`。 +- [x] 2026-05-15 已新增房间 RTC 业务抽象:`RoomRtcSessionIdentity`、`RoomRtcEngineAdapter`、`TrtcRoomRtcEngineAdapter`,将入房、角色切换、远端静音、音量回调、音乐混音统一收口到 TRTC 适配层。 +- [x] 2026-05-15 已重构 `rtc_manager.dart` 核心 RTC 链路:入房改为 `TRTCCloud.enterRoom(TRTCParams(... strRoomId ...), TRTCAppScene.voiceChatRoom)`;上麦/下麦改为 `switchRole(anchor/audience)` + `startLocalAudio/stopLocalAudio/muteLocalAudio`;音量回调改为按 TRTC `userId` 映射麦位。 +- [x] 2026-05-15 已移除 Agora token/续签链路:删除 `/external/im-trtc/tmp/agora/sig` 调用、`SCRtcTokenRes`、`getRtcToken` 仓储接口;TRTC 入房凭证统一复用登录态 `userSig`。 +- [x] 2026-05-15 已将房间音乐从 Agora `startAudioMixing/pause/resume/seek/volume` 切到 TRTC `TXAudioEffectManager`,保留本地播放、上麦发布、暂停恢复、seek 和音量能力。 +- [x] 2026-05-15 已更新配置口径:`agoraRtcAppid` 替换为 `trtcSdkAppId`,默认使用 `20036101`,支持 `--dart-define=TRTC_SDK_APP_ID=...` 覆盖。 +- [x] 2026-05-15 已更新原生侧:Android 移除 Agora 屏幕共享覆盖声明并改为 TRTC 屏幕采集组件移除,补 `BLUETOOTH_CONNECT` 与 Tencent ProGuard 规则;iOS `pod install` 已移除 Agora pods 并新增 `TXLiteAVSDK_Professional`。 +- [x] 2026-05-15 已补单测:覆盖 `roomAccount -> strRoomId`、业务 `userId`、TRTC 入房参数、角色切换、音乐混音参数;`flutter test test/room_rtc_types_test.dart test/trtc_room_rtc_engine_adapter_test.dart` 通过。 +- [x] 2026-05-15 已执行迁移残留扫描:`lib/android/ios/pubspec/test` 中无 `agora_rtc_engine`、Agora API、旧 token 接口残留;历史进度文档中的旧记录不作为代码残留。 +- [x] 2026-05-15 Android 打包链路已继续收口:`pag-1.0.7` 原包缺 Android namespace、Kotlin JVM target 不一致且仍引用 Flutter v1 embedding,已切为本地 patched package 并补齐 `namespace/jvmTarget/v2 embedding` 兼容,避免火箭 PAG 接入后阻断 APK 构建。 +- [x] 2026-05-15 已执行 `flutter build apk --debug`,结果通过,产物为 `build/app/outputs/flutter-apk/app-debug.apk`。 +- [x] 2026-05-15 复跑迁移单测通过;同时修复火箭奖励记录模型 `rocketIconUrl` getter 缺失导致的测试编译错误。 + +--- + - [x] 2026-05-14 用户确认:Figma 背景图、具体头像等仅用于截图校验,测试完成后不能留在包资源中 - [x] 2026-05-14 已删除校验专用资源:`sc_cp_progress_preview_bg.png`、`sc_cp_progress_sample_avatar.png` - [x] 2026-05-14 已移除 `RoomCpProgressDialogUser.avatarAsset` 以及预览入口对本地示例头像/背景图的引用