diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json index babe4dc..dbe1c4e 100644 --- a/assets/l10n/intl_ar.json +++ b/assets/l10n/intl_ar.json @@ -288,6 +288,7 @@ "rechargeAgency": "وكالة الشحن", "logout": "تسجيل الخروج", "adminCenter": "مركز الإدارة", + "managerCenter": "مركز المدير", "explore": "استكشاف", "micManagement": "إدارة الميكروفون", "followList": "قائمة المتابعة", diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json index efe58da..5d7f2df 100644 --- a/assets/l10n/intl_bn.json +++ b/assets/l10n/intl_bn.json @@ -442,6 +442,7 @@ "bdCenter": "BD সেন্টার", "rechargeAgency": "রিচার্জ এজেন্সি", "adminCenter": "অ্যাডমিন সেন্টার", + "managerCenter": "ম্যানেজার সেন্টার", "goldList": "সোনা তালিকা", "followList": "ফলো তালিকা", "fansList": "ফ্যান তালিকা", diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json index 896367b..b5bdbd4 100644 --- a/assets/l10n/intl_en.json +++ b/assets/l10n/intl_en.json @@ -412,6 +412,7 @@ "bdCenter": "BD Center", "rechargeAgency": "Recharge Agency", "adminCenter": "Admin Center", + "managerCenter": "Manager Center", "goldList": "Gold List", "followList": "Follow List", "fansList": "Fans List", diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json index f974a8e..7f4c89d 100644 --- a/assets/l10n/intl_tr.json +++ b/assets/l10n/intl_tr.json @@ -411,6 +411,7 @@ "bdCenter": "BD Merkezi", "rechargeAgency": "Yükleme Temsilciliği", "adminCenter": "Yönetici Merkezi", + "managerCenter": "Manager Center", "goldList": "Altın Listesi", "followList": "Takip Listesi", "fansList": "Hayran Listesi", diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index bf3a54a..49d0ef2 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -502,7 +502,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = S9X2AJ2US9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -693,7 +693,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -722,7 +722,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/lib/app/config/app_config.dart b/lib/app/config/app_config.dart index 02fb484..d4688f5 100644 --- a/lib/app/config/app_config.dart +++ b/lib/app/config/app_config.dart @@ -46,6 +46,9 @@ abstract class AppConfig { /// 管理员URL String get adminUrl; + /// Yumi管理中心URL + String get managerCenterUrl; + /// 代理中心URL String get agencyCenterUrl; diff --git a/lib/app/config/configs/sc_variant1_config.dart b/lib/app/config/configs/sc_variant1_config.dart index c4f0f2c..38e408c 100644 --- a/lib/app/config/configs/sc_variant1_config.dart +++ b/lib/app/config/configs/sc_variant1_config.dart @@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig { @override String get apiHost => const String.fromEnvironment( 'API_HOST', - defaultValue: 'https://jvapi.haiyihy.com/', + defaultValue: 'http://192.168.110.64:1100/', ); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 本地“http://192.168.110.64:1100/”,线上“https://jvapi.haiyihy.com/” @override @@ -55,6 +55,10 @@ class SCVariant1Config implements AppConfig { @override String get adminUrl => 'https://h5.haiyihy.com/admin-center/index.html'; // 正式 H5 页面 + @override + String get managerCenterUrl => + 'https://h5.haiyihy.com/hyapp/manager-center/index.html'; // 正式 H5 页面 + @override String get agencyCenterUrl => 'https://h5.haiyihy.com/agency-center/index.html'; // 正式 H5 页面 @@ -136,6 +140,7 @@ class SCVariant1Config implements AppConfig { 'imgHost': imgHost, 'privacyAgreementUrl': privacyAgreementUrl, 'userAgreementUrl': userAgreementUrl, + 'managerCenterUrl': managerCenterUrl, 'primaryColor': primaryColor, 'tencentImAppid': tencentImAppid, 'agoraRtcAppid': agoraRtcAppid, diff --git a/lib/app/constants/sc_global_config.dart b/lib/app/constants/sc_global_config.dart index d5906d6..2d9f73f 100644 --- a/lib/app/constants/sc_global_config.dart +++ b/lib/app/constants/sc_global_config.dart @@ -20,6 +20,7 @@ class SCGlobalConfig { static String get bdLeaderUrl => AppConfig.current.bdLeaderUrl; static String get coinSellerUrl => AppConfig.current.coinSellerUrl; static String get adminUrl => AppConfig.current.adminUrl; + static String get managerCenterUrl => AppConfig.current.managerCenterUrl; static String get agencyCenterUrl => AppConfig.current.agencyCenterUrl; static String get gamesKingUrl => AppConfig.current.gamesKingUrl; diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart index 8ca3d26..ad694ee 100644 --- a/lib/app_localizations.dart +++ b/lib/app_localizations.dart @@ -1025,6 +1025,8 @@ class SCAppLocalizations { String get adminCenter => translate('adminCenter'); + String get managerCenter => translate('managerCenter'); + String get confirm => translate('confirm'); String get cancel => translate('cancel'); diff --git a/lib/main.dart b/lib/main.dart index 7c1a6d5..6612033 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -46,7 +46,6 @@ import 'services/auth/user_profile_manager.dart'; import 'ui_kit/theme/socialchat_theme.dart'; import 'ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart'; import 'ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart'; -import 'ui_kit/widgets/svga/sc_svga_asset_widget.dart'; bool _isCrashlyticsReady = false; @@ -109,7 +108,6 @@ void _configureImageCache() { void _releaseVisualMemoryCaches({bool clearLiveImages = false}) { SCGiftVapSvgaManager().clearMemoryCache(); - SCSvgaAssetWidget.clearMemoryCache(); final imageCache = PaintingBinding.instance.imageCache; imageCache.clear(); if (clearLiveImages) { diff --git a/lib/modules/auth/account/sc_login_with_account_page.dart b/lib/modules/auth/account/sc_login_with_account_page.dart index da07961..4245325 100644 --- a/lib/modules/auth/account/sc_login_with_account_page.dart +++ b/lib/modules/auth/account/sc_login_with_account_page.dart @@ -16,7 +16,6 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/shared/tools/sc_version_utils.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:provider/provider.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; @@ -376,14 +375,16 @@ class SCLoginWithAccountPageState extends State } SCLoadingManager.show(context: context); try { - final results = await Future.wait([ - SCAccountRepository().loginForAccount(account, pass), - SCVersionUtils.checkReview(), - ]); - var user = (results[0] as SocialChatLoginRes); - if (!mounted) { - return; - } + var user = await SCAccountRepository().loginForAccount(account, pass); + if (!mounted) { + return; + } + AccountStorage().setCurrentUser(user); + final canContinue = await SCVersionUtils.checkReview(context: context); + if (!mounted || !canContinue) { + SCLoadingManager.hide(); + return; + } await Provider.of( context, listen: false, @@ -393,7 +394,6 @@ class SCLoginWithAccountPageState extends State if (!mounted) { return; } - AccountStorage().setCurrentUser(user); SCLoadingManager.hide(); try { await Future.wait([ diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 3251041..d018ffa 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/main.dart' show routeObserver; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart'; @@ -51,7 +52,7 @@ class VoiceRoomPage extends StatefulWidget { } class _VoiceRoomPageState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, RouteAware { static const int _chatTabCount = 3; static const Duration _luckyGiftComboWindow = Duration(seconds: 3); static const Duration _luckyGiftQueueDrainWindow = Duration(seconds: 3); @@ -76,11 +77,17 @@ class _VoiceRoomPageState extends State int _shownRoomStartupFailureToken = 0; RtcProvider? _rtcProvider; bool _roomProviderRebuildScheduled = false; + PageRoute? _routeObserverRoute; + bool _roomRouteVisible = false; @override void initState() { super.initState(); _tabController = TabController(length: _chatTabCount, vsync: this); + Provider.of( + context, + listen: false, + ).setVoiceRoomRouteVisible(true); _enableRoomVisualEffects(); _tabController.addListener(_handleTabChange); _subscription = eventBus.on().listen(( @@ -110,11 +117,23 @@ class _VoiceRoomPageState extends State rtcProvider.addListener(_handleRoomProviderChanged); context.read().attachRtcProvider(rtcProvider); } + final route = ModalRoute.of(context); + if (route is PageRoute && _routeObserverRoute != route) { + if (_routeObserverRoute != null) { + routeObserver.unsubscribe(this); + } + _routeObserverRoute = route; + routeObserver.subscribe(this, route); + _setRoomRouteVisible(route.isCurrent); + } _ensureRoomVisualEffectsEnabled(); } @override void dispose() { + routeObserver.unsubscribe(this); + _routeObserverRoute = null; + _setRoomRouteVisible(false); _rtcProvider?.removeListener(_handleRoomProviderChanged); _rtcProvider = null; _roomProviderRebuildScheduled = false; @@ -125,6 +144,36 @@ class _VoiceRoomPageState extends State super.dispose(); } + @override + void didPush() { + _setRoomRouteVisible(true); + } + + @override + void didPopNext() { + _setRoomRouteVisible(true); + } + + @override + void didPushNext() { + _setRoomRouteVisible(false); + } + + @override + void didPop() { + _setRoomRouteVisible(false); + } + + void _setRoomRouteVisible(bool visible) { + if (_roomRouteVisible == visible) { + return; + } + _roomRouteVisible = visible; + final rtcProvider = + _rtcProvider ?? Provider.of(context, listen: false); + rtcProvider.setVoiceRoomRouteVisible(visible); + } + void _handleTabChange() { if (!mounted) { return; @@ -168,6 +217,9 @@ class _VoiceRoomPageState extends State void _suspendRoomVisualEffects() { final rtcProvider = Provider.of(context, listen: false); + final currentRoomId = + rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; + rtcProvider.setVoiceRoomRouteVisible(false); rtcProvider.setRoomVisualEffectsEnabled(false); final rtmProvider = Provider.of(context, listen: false); @@ -184,7 +236,7 @@ class _VoiceRoomPageState extends State _clearGiftFlightBatchDedupeTimers(); _clearGiftVisualBatchDedupeTimers(); _giftSeatFlightController.clear(); - OverlayManager().removeRoom(); + OverlayManager().removeRoom(roomId: currentRoomId); SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend'); SCGiftVapSvgaManager().stopPlayback(); unawaited( diff --git a/lib/modules/room/voice_room_route.dart b/lib/modules/room/voice_room_route.dart index 6fc074d..8d9d90a 100644 --- a/lib/modules/room/voice_room_route.dart +++ b/lib/modules/room/voice_room_route.dart @@ -79,13 +79,15 @@ class VoiceRoomRoute implements SCIRouterProvider { final floatingGiftListener = rtmProvider.msgFloatingGiftListener; final luckyGiftRewardTickerListener = rtmProvider.msgLuckyGiftRewardTickerListener; + final currentRoomId = + rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; rtcProvider.setRoomVisualEffectsEnabled(false); rtmProvider.msgFloatingGiftListener = null; rtmProvider.msgLuckyGiftRewardTickerListener = null; RoomEntranceHelper.clearQueue(); giftAnimationManager.clearActiveAnimations(); - OverlayManager().removeRoom(); + OverlayManager().removeRoom(roomId: currentRoomId); SCRoomEffectScheduler().clearDeferredTasks(reason: 'room_background_route'); SCGiftVapSvgaManager().stopPlayback(); diff --git a/lib/modules/splash/splash_page.dart b/lib/modules/splash/splash_page.dart index 8117531..eed85c0 100644 --- a/lib/modules/splash/splash_page.dart +++ b/lib/modules/splash/splash_page.dart @@ -448,13 +448,13 @@ class _SplashPageState extends State { Future _goMainPage() async { try { - await SCVersionUtils.checkReview(); - if (!mounted) { - return; - } var user = AccountStorage().getCurrentUser(); var token = AccountStorage().getToken(); if (user != null && token.isNotEmpty) { + final canContinue = await SCVersionUtils.checkReview(context: context); + if (!mounted || !canContinue) { + return; + } SCNavigatorUtils.push(context, SCRoutes.home, replace: true); return; } @@ -464,6 +464,10 @@ class _SplashPageState extends State { } if (restoredUser != null && (restoredUser.token ?? "").trim().isNotEmpty) { + final canContinue = await SCVersionUtils.checkReview(context: context); + if (!mounted || !canContinue) { + return; + } SCNavigatorUtils.push(context, SCRoutes.home, replace: true); } else { SCNavigatorUtils.pushLoginIfNeeded(context, replace: true); diff --git a/lib/modules/user/me_page2.dart b/lib/modules/user/me_page2.dart index 1109bcd..2e1c519 100644 --- a/lib/modules/user/me_page2.dart +++ b/lib/modules/user/me_page2.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; @@ -50,6 +51,7 @@ class _MePage2State extends State { listen: false, ); profileManager.fetchUserProfileData(); + profileManager.getUserIdentity(); profileManager.balance(); _loadCounter(); _loadVipStatus(); @@ -660,6 +662,16 @@ class _MePage2State extends State { final items = <_MenuRowData>[]; final hasCurrentUser = userProfile.currentUserProfile != null; final isAgent = userProfile.userIdentity?.agent ?? false; + final isYumiManager = userProfile.userIdentity?.yumiManager ?? false; + if (kDebugMode) { + final currentProfile = userProfile.currentUserProfile; + debugPrint( + '[MePage2] menu identity userId=${currentProfile?.id}, ' + 'account=${currentProfile?.account}, hasCurrentUser=$hasCurrentUser, ' + 'agent=${userProfile.userIdentity?.agent}, admin=${userProfile.userIdentity?.admin}, ' + 'manager=${userProfile.userIdentity?.manager}, yumiManager=${userProfile.userIdentity?.yumiManager}', + ); + } // 1. 主播/代理入口 if (hasCurrentUser) { @@ -754,6 +766,25 @@ class _MePage2State extends State { ), ); } + if (isYumiManager) { + if (kDebugMode) { + debugPrint('[MePage2] show Manager Center'); + } + items.add( + _MenuRowData( + title: SCAppLocalizations.of(context)!.managerCenter, + assetIcon: 'sc_images/index/sc_icon_admin_center.png', + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.managerCenterUrl)}&showTitle=false", + ); + }, + ), + ); + } else if (kDebugMode) { + debugPrint('[MePage2] hide Manager Center because yumiManager=$isYumiManager'); + } return _buildMenuCard(items); } diff --git a/lib/modules/user/profile/profile_gift_wall_detail_page.dart b/lib/modules/user/profile/profile_gift_wall_detail_page.dart index 4b0865e..d24dcfb 100644 --- a/lib/modules/user/profile/profile_gift_wall_detail_page.dart +++ b/lib/modules/user/profile/profile_gift_wall_detail_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svga/flutter_svga.dart'; @@ -564,7 +566,6 @@ class _GiftWallSvgaEffectOverlay extends StatefulWidget { class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> with SingleTickerProviderStateMixin { late final SVGAAnimationController _controller; - MovieEntity? _retainedMovieEntity; bool _loaded = false; bool _finished = false; @@ -584,15 +585,9 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> Future _loadAndPlay() async { try { - final movieEntity = await SCGiftVapSvgaManager().acquireSvgaEntity( - widget.sourceUrl, - ); - if (!mounted || _finished) { - SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity); - return; - } + final movieEntity = await _loadMovieEntity(widget.sourceUrl); + if (!mounted || _finished) return; setState(() { - _retainedMovieEntity = movieEntity; _loaded = true; _controller.videoItem = movieEntity; }); @@ -604,6 +599,28 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> } } + Future _loadMovieEntity(String sourceUrl) async { + final cache = SCGiftVapSvgaManager().videoItemCache; + final cached = cache[sourceUrl]; + if (cached != null) return cached; + + final pathType = SCPathUtils.getPathType(sourceUrl); + late final MovieEntity movieEntity; + if (pathType == PathType.network) { + movieEntity = await SVGAParser.shared.decodeFromURL(sourceUrl); + } else if (pathType == PathType.asset) { + movieEntity = await SVGAParser.shared.decodeFromAssets(sourceUrl); + } else if (pathType == PathType.file) { + final bytes = await File(sourceUrl).readAsBytes(); + movieEntity = await SVGAParser.shared.decodeFromBuffer(bytes); + } else { + throw Exception("Unsupported SVGA source: $sourceUrl"); + } + movieEntity.autorelease = false; + cache[sourceUrl] = movieEntity; + return movieEntity; + } + void _finish() { if (_finished) return; _finished = true; @@ -614,8 +631,6 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> void dispose() { _controller.removeStatusListener(_handleStatusChanged); _controller.dispose(); - SCGiftVapSvgaManager().releaseSvgaEntity(_retainedMovieEntity); - _retainedMovieEntity = null; super.dispose(); } diff --git a/lib/modules/webview/webview_page.dart b/lib/modules/webview/webview_page.dart index 6f886c0..93a209d 100644 --- a/lib/modules/webview/webview_page.dart +++ b/lib/modules/webview/webview_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -48,6 +49,9 @@ class _WebViewPageState extends State { void initState() { super.initState(); urlLink = SCH5UrlUtils.appendAppParams(widget.url); + if (kDebugMode) { + debugPrint('[WebViewPage] load url=${_maskSensitiveUrl(urlLink)}'); + } // 初始化 WebViewController _controller = WebViewController() @@ -138,11 +142,21 @@ class _WebViewPageState extends State { }); }, onPageStarted: (String url) { + if (kDebugMode) { + debugPrint( + '[WebViewPage] page started url=${_maskSensitiveUrl(url)}', + ); + } setState(() { _isLoading = true; }); }, onPageFinished: (String url) { + if (kDebugMode) { + debugPrint( + '[WebViewPage] page finished url=${_maskSensitiveUrl(url)}', + ); + } // 延迟500毫秒注入,等待Vue3初始化 Future.delayed(Duration(milliseconds: 550), () { _injectJavaScriptInterface(); @@ -310,6 +324,23 @@ class _WebViewPageState extends State { } void _onWebResourceError(WebResourceError error) { + if (kDebugMode) { + debugPrint( + '[WebViewPage] resource error code=${error.errorCode}, ' + 'type=${error.errorType}, url=${_maskSensitiveUrl(error.url ?? '')}, ' + 'description=${error.description}', + ); + } // 可以在这里添加错误处理逻辑,比如显示错误页面 } + + String _maskSensitiveUrl(String url) { + final uri = Uri.tryParse(url); + if (uri == null) return url; + final params = Map.from(uri.queryParameters); + if (params.containsKey('token')) { + params['token'] = '***'; + } + return uri.replace(queryParameters: params).toString(); + } } diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 7dad717..157c611 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -311,6 +311,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { bool needUpDataUserInfo = false; bool _roomVisualEffectsEnabled = false; + bool _voiceRoomRouteVisible = false; bool _isExitingCurrentVoiceRoomSession = false; bool _isHandlingAgoraRoomFailure = false; bool _agoraJoined = false; @@ -435,6 +436,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled; + bool get isVoiceRoomRouteVisible => _voiceRoomRouteVisible; + bool get isExitingCurrentVoiceRoomSession => _isExitingCurrentVoiceRoomSession; @@ -479,6 +482,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { notifyListeners(); } + void setVoiceRoomRouteVisible(bool visible) { + _voiceRoomRouteVisible = visible; + } + void _setRoomStartupLoading() { _isHandlingRoomStartupFailure = false; _roomStartupFailureType = RoomStartupFailureType.none; @@ -3383,6 +3390,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { redPacketList.clear(); currenRoom = null; _roomVisualEffectsEnabled = false; + _voiceRoomRouteVisible = false; _isExitingCurrentVoiceRoomSession = false; isMic = true; isMusicPlaying = false; diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 0deaf09..8e00cd2 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -1697,6 +1697,7 @@ class RealTimeMessagingManager extends ChangeNotifier { final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; return rtcProvider.shouldShowRoomVisualEffects && + rtcProvider.isVoiceRoomRouteVisible && currentRoomId.isNotEmpty && currentRoomId == normalizedRoomId; } diff --git a/lib/services/auth/authentication_manager.dart b/lib/services/auth/authentication_manager.dart index 97d425c..47fd382 100644 --- a/lib/services/auth/authentication_manager.dart +++ b/lib/services/auth/authentication_manager.dart @@ -10,7 +10,6 @@ import 'package:yumi/shared/tools/sc_version_utils.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:provider/provider.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; @@ -64,12 +63,22 @@ class SocialChatAuthenticationManager extends ChangeNotifier { SCLoadingManager.show(); if (uid.isNotEmpty) { await DataPersistence.setPendingChannelAuth(authType, uid); - final res = await Future.wait([ - SCAccountRepository().loginForChannel(authType, uid), - SCVersionUtils.checkReview(), - ]); - var user = (res[0] as SocialChatLoginRes); + var user = await SCAccountRepository().loginForChannel(authType, uid); if (!context.mounted) { + SCLoadingManager.hide(); + return; + } + AccountStorage().setCurrentUser(user); + await DataPersistence.clearPendingChannelAuth(); + if (!context.mounted) { + SCLoadingManager.hide(); + return; + } + final canContinue = await SCVersionUtils.checkReview( + context: context, + ); + if (!context.mounted || !canContinue) { + SCLoadingManager.hide(); return; } await Provider.of( @@ -84,11 +93,6 @@ class SocialChatAuthenticationManager extends ChangeNotifier { if (!context.mounted) { return; } - AccountStorage().setCurrentUser(user); - await DataPersistence.clearPendingChannelAuth(); - if (!context.mounted) { - return; - } SCLoadingManager.hide(); notifyListeners(); SCNavigatorUtils.push(context, SCRoutes.home, replace: true); @@ -109,12 +113,22 @@ class SocialChatAuthenticationManager extends ChangeNotifier { SCLoadingManager.show(); if (uid.isNotEmpty) { await DataPersistence.setPendingChannelAuth(authType, uid); - final res = await Future.wait([ - SCAccountRepository().loginForChannel(authType, uid), - SCVersionUtils.checkReview(), - ]); - var user = (res[0] as SocialChatLoginRes); + var user = await SCAccountRepository().loginForChannel(authType, uid); if (!context.mounted) { + SCLoadingManager.hide(); + return; + } + AccountStorage().setCurrentUser(user); + await DataPersistence.clearPendingChannelAuth(); + if (!context.mounted) { + SCLoadingManager.hide(); + return; + } + final canContinue = await SCVersionUtils.checkReview( + context: context, + ); + if (!context.mounted || !canContinue) { + SCLoadingManager.hide(); return; } await Provider.of( @@ -129,11 +143,6 @@ class SocialChatAuthenticationManager extends ChangeNotifier { if (!context.mounted) { return; } - AccountStorage().setCurrentUser(user); - await DataPersistence.clearPendingChannelAuth(); - if (!context.mounted) { - return; - } SCLoadingManager.hide(); notifyListeners(); SCNavigatorUtils.push(context, SCRoutes.home, replace: true); diff --git a/lib/services/auth/user_profile_manager.dart b/lib/services/auth/user_profile_manager.dart index 7345dd5..1c9c287 100644 --- a/lib/services/auth/user_profile_manager.dart +++ b/lib/services/auth/user_profile_manager.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; @@ -95,8 +96,26 @@ class SocialChatUserProfileManager extends ChangeNotifier { SCUserIdentityRes? userIdentity; void getUserIdentity() async { - userIdentity = await SCAccountRepository().userIdentity(); - notifyListeners(); + if (kDebugMode) { + final profile = AccountStorage().getCurrentUser()?.userProfile; + debugPrint( + '[SCUserIdentity] request start userId=${profile?.id}, account=${profile?.account}', + ); + } + try { + userIdentity = await SCAccountRepository().userIdentity(); + if (kDebugMode) { + debugPrint( + '[SCUserIdentity] manager stored admin=${userIdentity?.admin}, ' + 'manager=${userIdentity?.manager}, yumiManager=${userIdentity?.yumiManager}', + ); + } + notifyListeners(); + } catch (error) { + if (kDebugMode) { + debugPrint('[SCUserIdentity] request failed error=$error'); + } + } } /// 当前正在查看的个人详情资料,不用于承载登录用户自己的 Me 页数据。 diff --git a/lib/shared/business_logic/models/res/sc_user_identity_res.dart b/lib/shared/business_logic/models/res/sc_user_identity_res.dart index 7abeba4..90f7aac 100644 --- a/lib/shared/business_logic/models/res/sc_user_identity_res.dart +++ b/lib/shared/business_logic/models/res/sc_user_identity_res.dart @@ -1,7 +1,7 @@ -/// agent : true -/// anchor : true -/// bd : false -/// freightAgent : false +// agent : true +// anchor : true +// bd : false +// freightAgent : false class SCUserIdentityRes { SCUserIdentityRes({ @@ -13,12 +13,14 @@ class SCUserIdentityRes { bool? bdLeader, bool? superFreightAgent, bool? manager, + bool? yumiManager, bool? freightAgent,}){ _agent = agent; _superFreightAgent = superFreightAgent; _anchor = anchor; _bd = bd; _manager = manager; + _yumiManager = yumiManager; _admin = admin; _superAdmin = superAdmin; _bdLeader = bdLeader; @@ -35,6 +37,9 @@ class SCUserIdentityRes { _admin = json['admin']; _freightAgent = json['freightAgent']; _manager = json['manager']; + _yumiManager = _boolValue( + json['yumi_manager'] ?? json['yumiManager'] ?? json['manager'], + ); } bool? _agent; bool? _anchor; @@ -44,13 +49,16 @@ class SCUserIdentityRes { bool? _bdLeader; bool? _freightAgent; bool? _manager; + bool? _yumiManager; bool? _superFreightAgent; SCUserIdentityRes copyWith({ bool? agent, bool? anchor, bool? bd, bool? admin, bool? superAdmin, + bool? bdLeader, bool? manager, + bool? yumiManager, bool? superFreightAgent, bool? freightAgent, }) => SCUserIdentityRes( agent: agent ?? _agent, @@ -60,6 +68,7 @@ SCUserIdentityRes copyWith({ bool? agent, bdLeader: bdLeader ?? _bdLeader, admin: admin ?? _admin, manager: manager ?? _manager, + yumiManager: yumiManager ?? _yumiManager, superFreightAgent: superFreightAgent ?? _superFreightAgent, freightAgent: freightAgent ?? _freightAgent, ); @@ -68,6 +77,7 @@ SCUserIdentityRes copyWith({ bool? agent, bool? get bd => _bd; bool? get admin => _admin; bool? get manager => _manager; + bool? get yumiManager => _yumiManager; bool? get superAdmin => _superAdmin; bool? get bdLeader => _bdLeader; bool? get superFreightAgent => _superFreightAgent; @@ -79,6 +89,7 @@ SCUserIdentityRes copyWith({ bool? agent, map['anchor'] = _anchor; map['bd'] = _bd; map['manager'] = _manager; + map['yumi_manager'] = _yumiManager; map['admin'] = _admin; map['superAdmin'] = _superAdmin; map['bdLeader'] = _bdLeader; @@ -87,4 +98,25 @@ SCUserIdentityRes copyWith({ bool? agent, return map; } -} \ No newline at end of file + static bool? _boolValue(dynamic value) { + if (value == null || value is bool) { + return value; + } + if (value is num) { + return value != 0; + } + if (value is String) { + final normalized = value.trim().toLowerCase(); + if (normalized.isEmpty) { + return null; + } + if (normalized == 'true' || normalized == '1' || normalized == 'yes') { + return true; + } + if (normalized == 'false' || normalized == '0' || normalized == 'no') { + return false; + } + } + return null; + } +} diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart index f897aad..df4fb58 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -470,15 +470,23 @@ class OverlayManager { _isProcessing = false; } - void removeRoom() { - _removeActiveRoomMessage(); + void removeRoom({String? roomId}) { + final normalizedRoomId = roomId?.trim() ?? ''; + _removeActiveRoomMessage( + roomId: normalizedRoomId, + includeRedPacket: normalizedRoomId.isNotEmpty, + ); _removeMessagesByType(1); _removeMessagesByType(0); _removeMessagesByType(5); + if (normalizedRoomId.isNotEmpty) { + _removeMessagesByType(4, roomId: normalizedRoomId); + } } // 辅助方法:移除特定类型的消息 - void _removeMessagesByType(int type) { + void _removeMessagesByType(int type, {String? roomId}) { + final normalizedRoomId = roomId?.trim() ?? ''; // 由于 PriorityQueue 没有直接的方法来移除特定元素, // 我们需要重建队列,排除指定类型的消息 final newQueue = SCPriorityQueue( @@ -488,7 +496,11 @@ class OverlayManager { // 遍历原始队列,只添加非指定类型的消息 while (_messageQueue.isNotEmpty) { final message = _messageQueue.removeFirst(); - if (message.type != type) { + final matchesType = message.type == type; + final matchesRoom = + normalizedRoomId.isEmpty || + (message.roomId ?? "").trim() == normalizedRoomId; + if (!matchesType || !matchesRoom) { newQueue.add(message); } } @@ -605,7 +617,8 @@ class OverlayManager { context, listen: false, ); - if (!rtcProvider.shouldShowRoomVisualEffects) { + if (!rtcProvider.shouldShowRoomVisualEffects || + !rtcProvider.isVoiceRoomRouteVisible) { return false; } final currentRoomId = @@ -633,6 +646,7 @@ class OverlayManager { final messageRoomId = (message.roomId ?? "").trim(); final isCurrentVisibleRoom = rtcProvider.shouldShowRoomVisualEffects && + rtcProvider.isVoiceRoomRouteVisible && currentRoomId.isNotEmpty && messageRoomId.isNotEmpty && currentRoomId == messageRoomId; @@ -640,6 +654,13 @@ class OverlayManager { // 区域红包在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。 return message.type == 4; } + final isCurrentRoomMessage = + currentRoomId.isNotEmpty && + messageRoomId.isNotEmpty && + currentRoomId == messageRoomId; + if (message.type == 4 && isCurrentRoomMessage) { + return false; + } if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) { return true; } @@ -684,12 +705,21 @@ class OverlayManager { _safeScheduleNext(); } - void _removeActiveRoomMessage() { + void _removeActiveRoomMessage({ + String? roomId, + bool includeRedPacket = false, + }) { final activeMessage = _currentMessage; + final normalizedRoomId = roomId?.trim() ?? ''; if (activeMessage == null || (activeMessage.type != 0 && activeMessage.type != 1 && - activeMessage.type != 5)) { + activeMessage.type != 5 && + !(includeRedPacket && activeMessage.type == 4))) { + return; + } + if (normalizedRoomId.isNotEmpty && + (activeMessage.roomId ?? "").trim() != normalizedRoomId) { return; } _removeActiveMessage(); diff --git a/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart index 2122ee7..0dc5bd2 100644 --- a/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart @@ -1,5 +1,7 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart'; @@ -296,7 +298,10 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository { Future versionManageLatest() async { final result = await http.get( "ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687", - fromJson: (json) => SCVersionManageLatestRes.fromJson(json), + fromJson: (json) { + _debugVersionResponse('releaseLatest', json); + return SCVersionManageLatestRes.fromJson(json); + }, ); return result; } @@ -306,11 +311,22 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository { Future versionManageLatestReview() async { final result = await http.get( "ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232", - fromJson: (json) => VersionManageLatesReviewRes.fromJson(json), + fromJson: (json) { + _debugVersionResponse('latestReview', json); + return VersionManageLatesReviewRes.fromJson(json); + }, ); return result; } + void _debugVersionResponse(String name, dynamic json) { + try { + debugPrint('[SCVersionUtils] $name rawBody=${jsonEncode(json)}'); + } catch (e) { + debugPrint('[SCVersionUtils] $name rawBody=$json encodeError=$e'); + } + } + ///sys/config/customer-service @override Future customerService() async { 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 63c9c09..2f97b17 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 @@ -607,7 +607,20 @@ class SCAccountRepository implements SocialChatUserRepository { final result = await http.get( "d29121f09fa26c45b5322df6de0fc25b", queryParams: parm, - fromJson: (json) => SCUserIdentityRes.fromJson(json), + fromJson: (json) { + final identity = SCUserIdentityRes.fromJson(json); + if (kDebugMode) { + debugPrint('[SCUserIdentity] raw=${jsonEncode(json)}'); + debugPrint( + '[SCUserIdentity] parsed admin=${identity.admin}, ' + 'manager=${identity.manager}, yumiManager=${identity.yumiManager}, ' + 'raw_manager=${json['manager']}(${json['manager']?.runtimeType}), ' + 'raw_yumi_manager=${json['yumi_manager']}(${json['yumi_manager']?.runtimeType}), ' + 'raw_yumiManager=${json['yumiManager']}(${json['yumiManager']?.runtimeType})', + ); + } + return identity; + }, ); return result; } diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index a5c35c5..67f02c5 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -23,8 +23,7 @@ class SCGiftVapSvgaManager { static const int _maxConsecutiveEntryTasksBeforeGift = 2; static const Duration _taskWatchdogTimeout = Duration(seconds: 20); static const Duration _entryTaskTtl = Duration(seconds: 6); - final Map _videoItemCache = {}; - final Map _svgaEntityUseCounts = {}; + Map videoItemCache = {}; static SCGiftVapSvgaManager? _inst; static const int _maxPreloadConcurrency = 1; static const int _maxPreloadQueueLength = 12; @@ -80,44 +79,6 @@ class SCGiftVapSvgaManager { return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga"; } - Future acquireSvgaEntity(String path) async { - final entity = await _loadSvgaEntity(path); - _retainSvgaEntity(entity); - return entity; - } - - void releaseSvgaEntity(MovieEntity? entity) { - if (entity == null) { - return; - } - final count = _svgaEntityUseCounts[entity] ?? 0; - if (count <= 1) { - _svgaEntityUseCounts.remove(entity); - if (!_videoItemCache.containsValue(entity) && - !_isSvgaEntityInUse(entity)) { - _disposeSvgaEntity(entity); - } else { - _trimSvgaCache(); - } - return; - } - _svgaEntityUseCounts[entity] = count - 1; - } - - void evictSvgaEntity(String path, {bool includeCurrent = false}) { - final entity = _videoItemCache.remove(path); - if (entity == null) { - return; - } - if (includeCurrent || !_isSvgaEntityInUse(entity)) { - _disposeSvgaEntity(entity); - } - } - - void _retainSvgaEntity(MovieEntity entity) { - _svgaEntityUseCounts[entity] = (_svgaEntityUseCounts[entity] ?? 0) + 1; - } - static int resolveEffectPriority({ int priority = 0, int type = giftEffectType, @@ -155,7 +116,7 @@ class SCGiftVapSvgaManager { bool _isPreloadedOrLoading(String path) { if (_needsSvgaController(path)) { - return _videoItemCache.containsKey(path) || + return videoItemCache.containsKey(path) || _svgaLoadTasks.containsKey(path); } final pathType = SCPathUtils.getPathType(path); @@ -295,19 +256,19 @@ class SCGiftVapSvgaManager { } MovieEntity? _touchCachedSvgaEntity(String path) { - final cached = _videoItemCache.remove(path); + final cached = videoItemCache.remove(path); if (cached != null) { - _videoItemCache[path] = cached; + videoItemCache[path] = cached; } return cached; } void _cacheSvgaEntity(String path, MovieEntity entity) { - final previous = _videoItemCache.remove(path); + final previous = videoItemCache.remove(path); if (previous != null && !identical(previous, entity)) { _disposeSvgaEntityIfUnused(previous); } - _videoItemCache[path] = entity; + videoItemCache[path] = entity; _trimSvgaCache(); } @@ -326,17 +287,17 @@ class SCGiftVapSvgaManager { } void _trimSvgaCache() { - while (_videoItemCache.length > _maxSvgaCacheEntries) { - final removableEntry = _videoItemCache.entries.firstWhere( + while (videoItemCache.length > _maxSvgaCacheEntries) { + final removableEntry = videoItemCache.entries.firstWhere( (entry) => !_isSvgaEntityInUse(entry.value), - orElse: () => _videoItemCache.entries.first, + orElse: () => videoItemCache.entries.first, ); if (_isSvgaEntityInUse(removableEntry.value)) { - _videoItemCache.remove(removableEntry.key); - _videoItemCache[removableEntry.key] = removableEntry.value; + videoItemCache.remove(removableEntry.key); + videoItemCache[removableEntry.key] = removableEntry.value; break; } - _videoItemCache.remove(removableEntry.key); + videoItemCache.remove(removableEntry.key); _disposeSvgaEntity(removableEntry.value); } } @@ -791,21 +752,20 @@ class SCGiftVapSvgaManager { _preloadQueue.clear(); _queuedPreloadPaths.clear(); _activePreloadCount = 0; - final entries = _videoItemCache.entries.toList(growable: false); + final entries = videoItemCache.entries.toList(growable: false); for (final entry in entries) { final entity = entry.value; if (!includeCurrent && _isSvgaEntityInUse(entity)) { continue; } - _videoItemCache.remove(entry.key); + videoItemCache.remove(entry.key); _disposeSvgaEntity(entity); } _playablePathCache.clear(); } bool _isSvgaEntityInUse(MovieEntity entity) { - return identical(_rsc?.videoItem, entity) || - (_svgaEntityUseCounts[entity] ?? 0) > 0; + return identical(_rsc?.videoItem, entity); } void _disposeSvgaEntityIfUnused(MovieEntity entity) { @@ -817,7 +777,6 @@ class SCGiftVapSvgaManager { void _disposeSvgaEntity(MovieEntity entity) { try { - _svgaEntityUseCounts.remove(entity); entity.dispose(); } catch (_) {} } diff --git a/lib/shared/tools/sc_version_utils.dart b/lib/shared/tools/sc_version_utils.dart index 7da88f1..054c920 100644 --- a/lib/shared/tools/sc_version_utils.dart +++ b/lib/shared/tools/sc_version_utils.dart @@ -1,14 +1,599 @@ -import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; - -class SCVersionUtils { - static Future checkReview() async { - try { - var versionInfo = await SCConfigRepositoryImp().versionManageLatestReview(); - if (versionInfo.review != null) { - SCGlobalConfig.isReview = - "${versionInfo.review?.version}" == SCGlobalConfig.version; - } - } catch (e) {} - } -} +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_version_manage_latest_res.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_url_launcher_utils.dart'; + +class SCVersionUtils { + static bool _forceUpdateDialogShowing = false; + + static Future checkReview({BuildContext? context}) async { + _debugLog( + 'checkReview start ' + 'currentVersion=${SCGlobalConfig.version} ' + 'currentBuild=${SCGlobalConfig.build} ' + 'platform=${Platform.isIOS ? "ios" : "android"} ' + 'apiHost=${SCGlobalConfig.apiHost} ' + 'hasContext=${context != null} ' + 'contextMounted=${context?.mounted}', + ); + try { + var versionInfo = + await SCConfigRepositoryImp().versionManageLatestReview(); + _debugLog( + 'checkReview response ' + 'review=${_versionInfoDebug(versionInfo.review)} ' + 'latest=${_versionInfoDebug(versionInfo.latest)}', + ); + if (versionInfo.review != null) { + SCGlobalConfig.isReview = + "${versionInfo.review?.version}" == SCGlobalConfig.version; + _debugLog('checkReview isReview=${SCGlobalConfig.isReview}'); + } + final latest = await _resolveLatestVersion(versionInfo.latest); + final shouldForceUpdate = latest != null && _shouldForceUpdate(latest); + _debugLog( + 'checkReview forceDecision ' + 'shouldForceUpdate=$shouldForceUpdate ' + 'reason=${_forceUpdateDecisionDebug(latest)}', + ); + if (context != null && + context.mounted && + latest != null && + shouldForceUpdate) { + SCLoadingManager.hide(); + _debugLog('checkReview showForceUpdateDialog'); + _showForceUpdateDialog(context, latest); + return false; + } + if (shouldForceUpdate && context == null) { + _debugLog('checkReview skip dialog reason=no_context'); + } else if (shouldForceUpdate && + context != null && + context.mounted == false) { + _debugLog('checkReview skip dialog reason=context_not_mounted'); + } else { + _debugLog('checkReview no force dialog'); + } + } catch (e) { + _debugLog('checkReview request failed error=$e'); + return true; + } + return true; + } + + static bool _shouldForceUpdate(SCVersionManageLatestRes latest) { + if (latest.forceUpdate != true) { + return false; + } + + final latestBuild = latest.buildVersion?.toInt() ?? 0; + final currentBuild = _currentComparableBuild(); + final hasLatestVersion = (latest.version ?? '').trim().isNotEmpty; + final versionCompare = _compareVersion( + latest.version, + SCGlobalConfig.version, + ); + if (hasLatestVersion) { + if (versionCompare > 0) { + return true; + } + if (versionCompare < 0) { + return false; + } + } + if (latestBuild > 0 && currentBuild > 0) { + return latestBuild > currentBuild; + } + + return hasLatestVersion && versionCompare > 0; + } + + static Future _resolveLatestVersion( + SCVersionManageLatestRes? latest, + ) async { + if (!_isVersionInfoEmpty(latest)) { + return latest; + } + _debugLog('checkReview latestReview latest empty, fallback releaseLatest'); + try { + final fallback = await SCConfigRepositoryImp().versionManageLatest(); + _debugLog('checkReview fallbackLatest=${_versionInfoDebug(fallback)}'); + return _isVersionInfoEmpty(fallback) ? latest : fallback; + } catch (e) { + _debugLog('checkReview fallback releaseLatest failed error=$e'); + return latest; + } + } + + static bool _isVersionInfoEmpty(SCVersionManageLatestRes? version) { + if (version == null) { + return true; + } + return (version.id ?? '').trim().isEmpty && + (version.platform ?? '').trim().isEmpty && + (version.version ?? '').trim().isEmpty && + version.forceUpdate == null && + (version.appType ?? '').trim().isEmpty && + version.review == null && + version.patch == null && + (version.updateDescribe ?? '').trim().isEmpty && + (version.updateWorshipDescribe ?? '').trim().isEmpty && + (version.downloadUrl ?? '').trim().isEmpty && + version.buildVersion == null && + version.createTime == null; + } + + static int _compareVersion(String? latestVersion, String currentVersion) { + final latestParts = _versionParts(latestVersion); + final currentParts = _versionParts(currentVersion); + final maxLength = + latestParts.length > currentParts.length + ? latestParts.length + : currentParts.length; + for (var i = 0; i < maxLength; i++) { + final latestPart = i < latestParts.length ? latestParts[i] : 0; + final currentPart = i < currentParts.length ? currentParts[i] : 0; + if (latestPart != currentPart) { + return latestPart.compareTo(currentPart); + } + } + return 0; + } + + static List _versionParts(String? version) { + final normalized = version?.trim() ?? ""; + if (normalized.isEmpty) { + return const []; + } + return normalized + .split(RegExp(r'[.+\-_]')) + .map( + (part) => int.tryParse(RegExp(r'^\d+').stringMatch(part) ?? '0') ?? 0, + ) + .toList(); + } + + static int _currentRawBuild() { + return int.tryParse(SCGlobalConfig.build.trim()) ?? 0; + } + + static int _currentComparableBuild() { + final rawBuild = _currentRawBuild(); + if (!Platform.isAndroid || rawBuild < 1000) { + return rawBuild; + } + + // Flutter split-per-abi APKs offset Android versionCode by ABI, e.g. + // build 132 becomes 2132 for arm64-v8a. The backend stores the base build. + final baseBuild = rawBuild % 1000; + return baseBuild > 0 ? baseBuild : rawBuild; + } + + static String _currentBuildDebug() { + final rawBuild = _currentRawBuild(); + final comparableBuild = _currentComparableBuild(); + if (rawBuild == comparableBuild) { + return '$rawBuild'; + } + return '$comparableBuild(raw=$rawBuild)'; + } + + static String _forceUpdateDecisionDebug(SCVersionManageLatestRes? latest) { + if (latest == null) { + return 'latest_null'; + } + final latestBuild = latest.buildVersion?.toInt() ?? 0; + final currentBuild = _currentComparableBuild(); + final currentBuildDebug = _currentBuildDebug(); + final versionCompare = _compareVersion( + latest.version, + SCGlobalConfig.version, + ); + if (latest.forceUpdate != true) { + return 'forceUpdate_not_true ' + 'forceUpdate=${latest.forceUpdate} ' + 'latestBuild=$latestBuild currentBuild=$currentBuildDebug ' + 'versionCompare=$versionCompare'; + } + if (latestBuild > 0 && currentBuild > 0) { + return 'compare_build ' + 'latestBuild=$latestBuild currentBuild=$currentBuildDebug ' + 'versionCompare=$versionCompare'; + } + return 'compare_version ' + 'latestVersion=${latest.version} ' + 'currentVersion=${SCGlobalConfig.version} ' + 'versionCompare=$versionCompare ' + 'latestBuild=$latestBuild currentBuild=$currentBuildDebug'; + } + + static String _versionInfoDebug(SCVersionManageLatestRes? version) { + if (version == null) { + return 'null'; + } + return '{' + 'id=${version.id}, ' + 'platform=${version.platform}, ' + 'version=${version.version}, ' + 'buildVersion=${version.buildVersion}, ' + 'forceUpdate=${version.forceUpdate}, ' + 'review=${version.review}, ' + 'patch=${version.patch}, ' + 'downloadUrl=${version.downloadUrl}' + '}'; + } + + static void _debugLog(String message) { + debugPrint('[SCVersionUtils] $message'); + } + + static void _showForceUpdateDialog( + BuildContext context, + SCVersionManageLatestRes latest, + ) { + if (_forceUpdateDialogShowing || !context.mounted) { + return; + } + _forceUpdateDialogShowing = true; + unawaited( + showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.black.withValues(alpha: 0.62), + builder: (dialogContext) { + final version = (latest.version ?? "").trim(); + final updateDescribe = (latest.updateDescribe ?? "").trim(); + final message = + updateDescribe.isNotEmpty + ? updateDescribe + : "A newer version is required to keep voice rooms stable " + "and secure."; + return PopScope( + canPop: false, + child: Dialog( + elevation: 0, + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 28), + child: SCForceUpdateDialog( + version: version, + message: message, + onUpdate: () { + unawaited(_openUpdateUrl(latest)); + }, + ), + ), + ); + }, + ).whenComplete(() { + _forceUpdateDialogShowing = false; + }), + ); + } + + static Future _openUpdateUrl(SCVersionManageLatestRes latest) async { + final url = _resolveDownloadUrl(latest); + if (url.isEmpty) { + return; + } + await SCUrlLauncherUtils.launchExternal(url); + } + + static String _resolveDownloadUrl(SCVersionManageLatestRes latest) { + final downloadUrl = (latest.downloadUrl ?? "").trim(); + if (downloadUrl.isNotEmpty) { + return downloadUrl; + } + return Platform.isIOS + ? SCGlobalConfig.appDownloadUrlApple + : SCGlobalConfig.appDownloadUrlGoogle; + } +} + +class SCForceUpdateDialog extends StatelessWidget { + const SCForceUpdateDialog({ + super.key, + required this.version, + required this.message, + required this.onUpdate, + }); + + final String version; + final String message; + final VoidCallback onUpdate; + + static const _asset = 'sc_images/general/sc_icon_app_update_bg.png'; + static const _primary = Color(0xff18F2B1); + static const _primaryDark = Color(0xff10C894); + static const _titleColor = Color(0xff151823); + static const _bodyColor = Color(0xff626775); + + @override + Widget build(BuildContext context) { + final constrainedTextScaler = MediaQuery.textScalerOf( + context, + ).clamp(maxScaleFactor: 1.15); + + return MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: constrainedTextScaler), + child: LayoutBuilder( + builder: (context, constraints) { + final width = + constraints.maxWidth.isFinite + ? constraints.maxWidth.clamp(280.0, 340.0) + : 340.0; + + return Center( + child: ConstrainedBox( + constraints: BoxConstraints.tightFor(width: width), + child: ClipRRect( + borderRadius: BorderRadius.circular(28), + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.18), + blurRadius: 34, + offset: const Offset(0, 18), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 164, + width: double.infinity, + child: Image.asset( + _asset, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + frameBuilder: ( + context, + child, + frame, + wasSynchronouslyLoaded, + ) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + return const _UpdateHeroFallback(); + }, + errorBuilder: (context, error, stackTrace) { + return const _UpdateHeroFallback(); + }, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _VersionPill(version: version), + const SizedBox(height: 14), + const Text( + 'Update Required', + textAlign: TextAlign.center, + style: TextStyle( + color: _titleColor, + fontSize: 22, + fontWeight: FontWeight.w800, + height: 1.16, + ), + ), + const SizedBox(height: 10), + Text( + message, + maxLines: 3, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle( + color: _bodyColor, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.45, + ), + ), + const SizedBox(height: 18), + const _UpdateNotice(), + const SizedBox(height: 22), + _UpdateButton(onPressed: onUpdate), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ); + } +} + +class _UpdateHeroFallback extends StatelessWidget { + const _UpdateHeroFallback(); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xff14D6A1), Color(0xff65F3CD)], + ), + ), + child: Stack( + children: [ + Positioned( + left: 34, + top: 26, + child: Icon( + Icons.auto_awesome_rounded, + color: Colors.white.withValues(alpha: 0.72), + size: 18, + ), + ), + Positioned( + right: 44, + top: 36, + child: Icon( + Icons.auto_awesome_rounded, + color: Colors.white.withValues(alpha: 0.58), + size: 14, + ), + ), + Center( + child: Container( + width: 86, + height: 86, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.92), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.12), + blurRadius: 18, + offset: const Offset(0, 10), + ), + ], + ), + child: const Icon( + Icons.rocket_launch_rounded, + color: SCForceUpdateDialog._primaryDark, + size: 46, + ), + ), + ), + ], + ), + ); + } +} + +class _VersionPill extends StatelessWidget { + const _VersionPill({required this.version}); + + final String version; + + @override + Widget build(BuildContext context) { + final label = + version.isEmpty ? 'New version available' : 'Version $version'; + + return DecoratedBox( + decoration: BoxDecoration( + color: SCForceUpdateDialog._primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: SCForceUpdateDialog._primary.withValues(alpha: 0.28), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: SCForceUpdateDialog._primaryDark, + fontSize: 12, + fontWeight: FontWeight.w700, + height: 1.1, + ), + ), + ), + ); + } +} + +class _UpdateNotice extends StatelessWidget { + const _UpdateNotice(); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: const Color(0xffF6F8FA), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: const Text( + 'This Agora-based version has expired. Please update to the latest ' + 'version to continue using voice rooms normally.', + textAlign: TextAlign.center, + style: TextStyle( + color: SCForceUpdateDialog._titleColor, + fontSize: 13, + fontWeight: FontWeight.w600, + height: 1.36, + ), + ), + ), + ); + } +} + +class _UpdateButton extends StatelessWidget { + const _UpdateButton({required this.onPressed}); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Ink( + height: 50, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [SCForceUpdateDialog._primary, Color(0xff76FFD8)], + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: SCForceUpdateDialog._primary.withValues(alpha: 0.34), + blurRadius: 16, + offset: const Offset(0, 8), + ), + ], + ), + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(16), + child: const Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Update Now', + style: TextStyle( + color: Color(0xff10131A), + fontSize: 16, + fontWeight: FontWeight.w800, + height: 1, + ), + ), + SizedBox(width: 8), + Icon( + Icons.arrow_forward_rounded, + color: Color(0xff10131A), + size: 20, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/headdress/headdress_widget.dart b/lib/ui_kit/widgets/headdress/headdress_widget.dart index e4f97a7..e5f0868 100644 --- a/lib/ui_kit/widgets/headdress/headdress_widget.dart +++ b/lib/ui_kit/widgets/headdress/headdress_widget.dart @@ -48,8 +48,6 @@ class SVGAHeadwearWidget extends StatefulWidget { class _SVGAHeadwearWidgetState extends State with SingleTickerProviderStateMixin { SVGAAnimationController? _animationController; - MovieEntity? _retainedMovieEntity; - String? _retainedResource; bool _isLoading = true; bool _isNetworkResource = false; bool _hasError = false; @@ -84,53 +82,41 @@ class _SVGAHeadwearWidgetState extends State }); try { + // 检查缓存 + MovieEntity? videoItem; if (widget.useCache) { - final videoItem = - _retainedResource == widget.resource && _retainedMovieEntity != null - ? _retainedMovieEntity! - : await SCGiftVapSvgaManager().acquireSvgaEntity( - widget.resource, - ); - final acquiredNewEntity = !identical(videoItem, _retainedMovieEntity); - if (!mounted) { - if (acquiredNewEntity) { - SCGiftVapSvgaManager().releaseSvgaEntity(videoItem); - } - return; - } + videoItem = SCGiftVapSvgaManager().videoItemCache[widget.resource]; + } - setState(() { - _releaseRetainedMovieEntity(except: videoItem); - _retainedMovieEntity = videoItem; - _retainedResource = widget.resource; - _animationController?.videoItem = videoItem; - _isLoading = false; - }); - } else { - final videoItem = + // 如果没有缓存,则进行解析 + if (videoItem == null) { + videoItem = _isNetworkResource ? await SVGAParser.shared.decodeFromURL(widget.resource) : await SVGAParser.shared.decodeFromAssets(widget.resource); - if (!mounted) { - videoItem.dispose(); - return; + videoItem.autorelease = false; + // 存入缓存 + if (widget.useCache) { + SCGiftVapSvgaManager().videoItemCache[widget.resource] = videoItem; } + } + if (mounted) { setState(() { _animationController?.videoItem = videoItem; _isLoading = false; }); - } - // 根据循环次数设置播放方式 - if (widget.loops == 0) { - _animationController?.repeat(); - } else { - _animationController?.repeat(count: widget.loops); - } + // 根据循环次数设置播放方式 + if (widget.loops == 0) { + _animationController?.repeat(); + } else { + _animationController?.repeat(count: widget.loops); + } - if (widget.onFinishLoading != null) { - widget.onFinishLoading!(); + if (widget.onFinishLoading != null) { + widget.onFinishLoading!(); + } } } catch (e) { if (mounted) { @@ -171,16 +157,14 @@ class _SVGAHeadwearWidgetState extends State _animationController?.stop(); if (widget.clearsAfterStop) { _animationController?.videoItem = null; - _releaseRetainedMovieEntity(); } } // 重新加载动画 void reload() { if (widget.useCache) { - _animationController?.videoItem = null; - _releaseRetainedMovieEntity(); - SCGiftVapSvgaManager().evictSvgaEntity(widget.resource); + // 清除缓存项 + SCGiftVapSvgaManager().videoItemCache.remove(widget.resource); } _loadAnimation(); } @@ -189,20 +173,9 @@ class _SVGAHeadwearWidgetState extends State void dispose() { stop(); _animationController?.dispose(); - _releaseRetainedMovieEntity(); super.dispose(); } - void _releaseRetainedMovieEntity({MovieEntity? except}) { - final entity = _retainedMovieEntity; - if (entity == null || identical(entity, except)) { - return; - } - _retainedMovieEntity = null; - _retainedResource = null; - SCGiftVapSvgaManager().releaseSvgaEntity(entity); - } - @override Widget build(BuildContext context) { if (_isLoading && !widget.autoPlay) { diff --git a/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart b/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart index 6c954fc..11b57c0 100644 --- a/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart +++ b/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart @@ -270,8 +270,6 @@ class _RoomEntranceWidgetState extends State bool _isLoading = false; bool _hasError = false; String? _currentResource; - MovieEntity? _retainedMovieEntity; - String? _retainedResource; // 用于存储SVGAImage组件的key,以便刷新占位符 GlobalKey _svgaKey = GlobalKey(); @@ -353,79 +351,45 @@ class _RoomEntranceWidgetState extends State _hasError = false; }); - MovieEntity? acquiredMovieEntity; try { + final isNetworkResource = resource.startsWith('http'); + MovieEntity? videoItem; + if (widget.useCache) { - final videoItem = - _retainedResource == resource && _retainedMovieEntity != null - ? _retainedMovieEntity! - : await SCGiftVapSvgaManager().acquireSvgaEntity(resource); - final acquiredNewEntity = !identical(videoItem, _retainedMovieEntity); - if (acquiredNewEntity) { - acquiredMovieEntity = videoItem; - } - if (!mounted) { - if (acquiredNewEntity) { - SCGiftVapSvgaManager().releaseSvgaEntity(videoItem); - acquiredMovieEntity = null; - } - return; - } + videoItem = SCGiftVapSvgaManager().videoItemCache[resource]; + } - // 应用动态数据(文本、图片替换) - await _applyDynamicData(videoItem, dynamicData); - if (!mounted) { - if (acquiredNewEntity) { - SCGiftVapSvgaManager().releaseSvgaEntity(videoItem); - acquiredMovieEntity = null; - } - return; - } - - setState(() { - _releaseRetainedMovieEntity(except: videoItem); - _retainedMovieEntity = videoItem; - _retainedResource = resource; - _animationController?.videoItem = videoItem; - _isLoading = false; - }); - acquiredMovieEntity = null; - } else { - final isNetworkResource = resource.startsWith('http'); - final videoItem = + if (videoItem == null) { + videoItem = isNetworkResource ? await SVGAParser.shared.decodeFromURL(resource) : await SVGAParser.shared.decodeFromAssets(resource); - if (!mounted) { - videoItem.dispose(); - return; - } + videoItem.autorelease = false; + if (widget.useCache) { + SCGiftVapSvgaManager().videoItemCache[resource] = videoItem; + } + } + + if (mounted) { // 应用动态数据(文本、图片替换) await _applyDynamicData(videoItem, dynamicData); - if (!mounted) { - videoItem.dispose(); - return; - } setState(() { _animationController?.videoItem = videoItem; _isLoading = false; }); - } - // 开始播放动画 - _startAnimation(); + // 开始播放动画 + _startAnimation(); - if (widget.onFinishLoading != null) { - widget.onFinishLoading!(); + if (widget.onFinishLoading != null) { + widget.onFinishLoading!(); + } } } catch (e) { - SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity); if (mounted) { setState(() { - _animationController?.videoItem = null; - _releaseRetainedMovieEntity(); _isLoading = false; _hasError = true; }); @@ -579,7 +543,6 @@ class _RoomEntranceWidgetState extends State _animationController?.stop(); if (widget.clearsAfterStop) { _animationController?.videoItem = null; - _releaseRetainedMovieEntity(); setState(() { _currentResource = null; }); @@ -590,9 +553,7 @@ class _RoomEntranceWidgetState extends State void reload() { if (_currentResource != null) { if (widget.useCache) { - _animationController?.videoItem = null; - _releaseRetainedMovieEntity(); - SCGiftVapSvgaManager().evictSvgaEntity(_currentResource!); + SCGiftVapSvgaManager().videoItemCache.remove(_currentResource); } _loadAnimation( @@ -607,22 +568,11 @@ class _RoomEntranceWidgetState extends State void dispose() { stop(); _animationController?.dispose(); - _releaseRetainedMovieEntity(); // 从队列管理器中移除当前播放器 _queueManager.clearCurrentPlayer(); super.dispose(); } - void _releaseRetainedMovieEntity({MovieEntity? except}) { - final entity = _retainedMovieEntity; - if (entity == null || identical(entity, except)) { - return; - } - _retainedMovieEntity = null; - _retainedResource = null; - SCGiftVapSvgaManager().releaseSvgaEntity(entity); - } - @override Widget build(BuildContext context) { // 显示加载状态 diff --git a/lib/ui_kit/widgets/room/exit_min_room_page.dart b/lib/ui_kit/widgets/room/exit_min_room_page.dart index 847e9de..2cdab5b 100644 --- a/lib/ui_kit/widgets/room/exit_min_room_page.dart +++ b/lib/ui_kit/widgets/room/exit_min_room_page.dart @@ -74,11 +74,16 @@ class _ExitMinRoomPageState extends State { final giftAnimationManager = context.read(); final rtmProvider = context.read(); + final currentRoomId = + rtcProvider.currenRoom?.roomProfile?.roomProfile?.id + ?.trim() ?? + ''; + rtcProvider.setVoiceRoomRouteVisible(false); rtcProvider.setRoomVisualEffectsEnabled(false); rtmProvider.msgFloatingGiftListener = null; rtmProvider.msgLuckyGiftRewardTickerListener = null; RoomEntranceHelper.clearQueue(); - OverlayManager().removeRoom(); + OverlayManager().removeRoom(roomId: currentRoomId); SCRoomEffectScheduler().clearDeferredTasks( reason: 'room_minimize', ); diff --git a/lib/ui_kit/widgets/room/room_play_widget.dart b/lib/ui_kit/widgets/room/room_play_widget.dart index 3d30818..6dd0220 100644 --- a/lib/ui_kit/widgets/room/room_play_widget.dart +++ b/lib/ui_kit/widgets/room/room_play_widget.dart @@ -22,6 +22,8 @@ class RoomPlayWidget extends StatefulWidget { } class _RoomPlayWidgetState extends State { + static const double _rightActionDownShift = 24; + @override Widget build(BuildContext context) { return SizedBox.expand( @@ -29,17 +31,17 @@ class _RoomPlayWidgetState extends State { children: [ PositionedDirectional( end: 15.w, - bottom: 150.w, + bottom: (150 - _rightActionDownShift).w, child: const RoomMusicFloatingEntry(), ), PositionedDirectional( end: 15.w, - bottom: 200.w, + bottom: (200 - _rightActionDownShift).w, child: const _RoomRedPacketFloatingEntry(), ), PositionedDirectional( end: 15.w, - bottom: 100.w, + bottom: (100 - _rightActionDownShift).w, child: const RoomGameEntryButton(), ), PositionedDirectional( diff --git a/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart b/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart index d497d0f..a693f6b 100644 --- a/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart +++ b/lib/ui_kit/widgets/svga/sc_network_svga_widget.dart @@ -52,8 +52,6 @@ class _SCNetworkSvgaWidgetState extends State with SingleTickerProviderStateMixin { late final SVGAAnimationController _controller; String? _loadedResource; - MovieEntity? _retainedMovieEntity; - String? _retainedResource; bool _hasError = false; @override @@ -80,10 +78,9 @@ class _SCNetworkSvgaWidgetState extends State final resource = widget.resource.trim(); if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) { setState(() { - _controller.videoItem = null; - _releaseRetainedMovieEntity(); _hasError = true; _loadedResource = null; + _controller.videoItem = null; }); return; } @@ -92,53 +89,37 @@ class _SCNetworkSvgaWidgetState extends State _hasError = false; }); - MovieEntity? acquiredMovieEntity; try { - final movieEntity = - _retainedResource == resource && _retainedMovieEntity != null - ? _retainedMovieEntity! - : await SCGiftVapSvgaManager().acquireSvgaEntity(resource); - final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity); - if (acquiredNewEntity) { - acquiredMovieEntity = movieEntity; - } - if (!mounted || widget.resource.trim() != resource) { - if (acquiredNewEntity) { - SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity); - acquiredMovieEntity = null; - } - return; + final cache = SCGiftVapSvgaManager().videoItemCache; + var movieEntity = cache[resource]; + if (movieEntity == null) { + movieEntity = + resource.startsWith("http") + ? await SVGAParser.shared.decodeFromURL(resource) + : await SVGAParser.shared.decodeFromAssets(resource); + movieEntity.autorelease = false; + cache[resource] = movieEntity; } if (widget.movieConfigurer != null) { movieEntity.dynamicItem.reset(); await widget.movieConfigurer!(movieEntity); } if (!mounted || widget.resource.trim() != resource) { - if (acquiredNewEntity) { - SCGiftVapSvgaManager().releaseSvgaEntity(movieEntity); - acquiredMovieEntity = null; - } return; } setState(() { - _releaseRetainedMovieEntity(except: movieEntity); - _retainedMovieEntity = movieEntity; - _retainedResource = resource; _loadedResource = resource; _controller.videoItem = movieEntity; }); - acquiredMovieEntity = null; _syncPlayback(restartIfActive: true); } catch (error) { - SCGiftVapSvgaManager().releaseSvgaEntity(acquiredMovieEntity); if (!mounted || widget.resource.trim() != resource) { return; } setState(() { - _controller.videoItem = null; - _releaseRetainedMovieEntity(); _hasError = true; _loadedResource = null; + _controller.videoItem = null; }); } } @@ -164,20 +145,9 @@ class _SCNetworkSvgaWidgetState extends State @override void dispose() { _controller.dispose(); - _releaseRetainedMovieEntity(); super.dispose(); } - void _releaseRetainedMovieEntity({MovieEntity? except}) { - final entity = _retainedMovieEntity; - if (entity == null || identical(entity, except)) { - return; - } - _retainedMovieEntity = null; - _retainedResource = null; - SCGiftVapSvgaManager().releaseSvgaEntity(entity); - } - @override Widget build(BuildContext context) { final videoItem = _controller.videoItem; diff --git a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart index 2d19343..84be820 100644 --- a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart +++ b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart @@ -8,11 +8,9 @@ typedef SCSvgaMovieConfigurer = FutureOr Function(MovieEntity movieEntity); class SCSvgaAssetWidget extends StatefulWidget { - static const int _maxCacheEntries = 8; static final Map _cache = {}; static final Map> _loadingTasks = >{}; - static final Map _activeUseCounts = {}; const SCSvgaAssetWidget({ super.key, @@ -51,26 +49,8 @@ class SCSvgaAssetWidget extends StatefulWidget { return _obtainMovieEntity(assetPath); } - static void clearMemoryCache({bool includeActive = false}) { - final entries = _cache.entries.toList(growable: false); - for (final entry in entries) { - final entity = entry.value; - if (!includeActive && _isActive(entity)) { - continue; - } - _cache.remove(entry.key); - _disposeMovieEntity(entity); - } - } - - static Future _acquireMovieEntity(String assetPath) async { - final entity = await _obtainMovieEntity(assetPath); - _retainMovieEntity(entity); - return entity; - } - static Future _obtainMovieEntity(String assetPath) async { - final cached = _touchCachedMovieEntity(assetPath); + final cached = _cache[assetPath]; if (cached != null) { return cached; } @@ -91,7 +71,7 @@ class SCSvgaAssetWidget extends StatefulWidget { ); } entity.autorelease = false; - _cacheMovieEntity(assetPath, entity); + _cache[assetPath] = entity; return entity; }(); @@ -103,77 +83,6 @@ class SCSvgaAssetWidget extends StatefulWidget { } } - static MovieEntity? _touchCachedMovieEntity(String assetPath) { - final cached = _cache.remove(assetPath); - if (cached != null) { - _cache[assetPath] = cached; - } - return cached; - } - - static void _cacheMovieEntity(String assetPath, MovieEntity entity) { - final previous = _cache.remove(assetPath); - if (previous != null && !identical(previous, entity)) { - _disposeMovieEntityIfInactive(previous); - } - _cache[assetPath] = entity; - _trimCache(); - } - - static void _trimCache() { - while (_cache.length > _maxCacheEntries) { - final removableEntry = _cache.entries.firstWhere( - (entry) => !_isActive(entry.value), - orElse: () => _cache.entries.first, - ); - if (_isActive(removableEntry.value)) { - _cache.remove(removableEntry.key); - _cache[removableEntry.key] = removableEntry.value; - break; - } - _cache.remove(removableEntry.key); - _disposeMovieEntity(removableEntry.value); - } - } - - static void _retainMovieEntity(MovieEntity entity) { - _activeUseCounts[entity] = (_activeUseCounts[entity] ?? 0) + 1; - } - - static void _releaseMovieEntity(MovieEntity? entity) { - if (entity == null) { - return; - } - final count = _activeUseCounts[entity] ?? 0; - if (count <= 1) { - _activeUseCounts.remove(entity); - if (!_cache.containsValue(entity)) { - _disposeMovieEntity(entity); - } else { - _trimCache(); - } - return; - } - _activeUseCounts[entity] = count - 1; - } - - static bool _isActive(MovieEntity entity) => - (_activeUseCounts[entity] ?? 0) > 0; - - static void _disposeMovieEntityIfInactive(MovieEntity entity) { - if (_isActive(entity)) { - return; - } - _disposeMovieEntity(entity); - } - - static void _disposeMovieEntity(MovieEntity entity) { - try { - _activeUseCounts.remove(entity); - entity.dispose(); - } catch (_) {} - } - @override State createState() => _SCSvgaAssetWidgetState(); } @@ -182,8 +91,6 @@ class _SCSvgaAssetWidgetState extends State with SingleTickerProviderStateMixin { late final SVGAAnimationController _controller; String? _loadedAssetPath; - MovieEntity? _retainedMovieEntity; - String? _retainedAssetPath; bool _hasError = false; @override @@ -224,44 +131,25 @@ class _SCSvgaAssetWidgetState extends State setState(() { _hasError = false; }); - MovieEntity? acquiredMovieEntity; try { - final movieEntity = - _retainedAssetPath == assetPath && _retainedMovieEntity != null - ? _retainedMovieEntity! - : await SCSvgaAssetWidget._acquireMovieEntity(assetPath); - final acquiredNewEntity = !identical(movieEntity, _retainedMovieEntity); - if (acquiredNewEntity) { - acquiredMovieEntity = movieEntity; - } + final movieEntity = await SCSvgaAssetWidget._obtainMovieEntity(assetPath); if (widget.movieConfigurer != null) { movieEntity.dynamicItem.reset(); await widget.movieConfigurer!(movieEntity); } if (!mounted || widget.assetPath != assetPath) { - if (acquiredNewEntity) { - SCSvgaAssetWidget._releaseMovieEntity(movieEntity); - acquiredMovieEntity = null; - } return; } setState(() { - _releaseRetainedMovieEntity(except: movieEntity); - _retainedMovieEntity = movieEntity; - _retainedAssetPath = assetPath; _loadedAssetPath = assetPath; _controller.videoItem = movieEntity; }); - acquiredMovieEntity = null; _syncPlayback(restartIfActive: true); } catch (error) { - SCSvgaAssetWidget._releaseMovieEntity(acquiredMovieEntity); if (!mounted || widget.assetPath != assetPath) { return; } setState(() { - _controller.videoItem = null; - _releaseRetainedMovieEntity(); _hasError = true; }); } @@ -297,20 +185,9 @@ class _SCSvgaAssetWidgetState extends State void dispose() { _controller.removeStatusListener(_handleAnimationStatusChanged); _controller.dispose(); - _releaseRetainedMovieEntity(); super.dispose(); } - void _releaseRetainedMovieEntity({MovieEntity? except}) { - final entity = _retainedMovieEntity; - if (entity == null || identical(entity, except)) { - return; - } - _retainedMovieEntity = null; - _retainedAssetPath = null; - SCSvgaAssetWidget._releaseMovieEntity(entity); - } - @override Widget build(BuildContext context) { if (_hasError) { diff --git a/pubspec.yaml b/pubspec.yaml index 37c5e1f..2bbd8b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.3.1+12 +version: 1.3.1+131 environment: