diff --git a/lib/debug/lucky_gift_global_floating_preview.dart b/lib/debug/lucky_gift_global_floating_preview.dart new file mode 100644 index 0000000..43e5c18 --- /dev/null +++ b/lib/debug/lucky_gift_global_floating_preview.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:pag/pag.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart'; + +void main() { + runApp(const LuckyGiftGlobalFloatingPreviewApp()); +} + +class LuckyGiftGlobalFloatingPreviewApp extends StatelessWidget { + const LuckyGiftGlobalFloatingPreviewApp({super.key}); + + @override + Widget build(BuildContext context) { + return ScreenUtilInit( + designSize: const Size(375, 812), + minTextAdapt: true, + builder: (_, child) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: const _LuckyGiftGlobalFloatingPreviewPage(), + ); + }, + ); + } +} + +class _LuckyGiftGlobalFloatingPreviewPage extends StatelessWidget { + const _LuckyGiftGlobalFloatingPreviewPage(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + Positioned.fill( + child: Image.asset(_backgroundAsset, fit: BoxFit.cover), + ), + Positioned( + left: 0, + top: 116.w, + child: FloatingLuckyGiftGlobalScreenWidget( + debugDisableAutoSlide: true, + message: SCFloatingMessage( + type: FloatingLuckyGiftGlobalScreenWidget.floatingType, + roomId: '1002900', + userName: 'Bajul', + userAvatarUrl: '', + coins: 1000, + multiple: 10, + ), + onAnimationCompleted: () {}, + ), + ), + Positioned(left: 0, top: 250.w, child: const _PagLayerScanner()), + ], + ), + ); + } +} + +const _backgroundAsset = 'sc_images/room/background_examples/bg_example_1.png'; + +class _PagLayerScanner extends StatefulWidget { + const _PagLayerScanner(); + + @override + State<_PagLayerScanner> createState() => _PagLayerScannerState(); +} + +class _PagLayerScannerState extends State<_PagLayerScanner> { + final _pagKey = GlobalKey(); + final _foundLayers = {}; + String _summary = 'loading PAG...'; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w, + height: 160.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w, + height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w, + child: PAGView.asset( + FloatingLuckyGiftGlobalScreenWidget.backgroundPagAssetPath, + key: _pagKey, + width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w, + height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w, + autoPlay: false, + initProgress: 0.35, + repeatCount: PAGView.REPEAT_COUNT_LOOP, + onInit: _scanLayers, + ), + ), + Container( + width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w, + padding: EdgeInsets.all(6.w), + color: Colors.black.withValues(alpha: 0.65), + child: Text( + _summary, + maxLines: 4, + overflow: TextOverflow.ellipsis, + textScaler: TextScaler.noScaling, + style: TextStyle( + color: Colors.white, + fontSize: 9.sp, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ); + } + + Future _scanLayers() async { + await Future.delayed(const Duration(milliseconds: 200)); + final state = _pagKey.currentState; + if (state == null) { + return; + } + final rawWidth = state.rawWidth; + final rawHeight = state.rawHeight; + debugPrint('[LuckyGiftPAG] rawSize=${rawWidth}x$rawHeight'); + if (rawWidth <= 0 || rawHeight <= 0) { + return; + } + final sampleXs = + [ + 40, + 80, + 120, + 170, + 220, + 280, + 340, + 410, + 480, + 560, + 640, + 710, + ].where((value) => value < rawWidth).toList(); + final sampleYs = + [ + 32, + 58, + 82, + 104, + 128, + 158, + ].where((value) => value < rawHeight).toList(); + for (final y in sampleYs) { + for (final x in sampleXs) { + final layers = await state.getLayersUnderPoint(x, y); + if (layers.isEmpty) { + continue; + } + _foundLayers.addAll(layers.where((name) => name.trim().isNotEmpty)); + debugPrint( + '[LuckyGiftPAG] point=(${x.toStringAsFixed(0)},' + '${y.toStringAsFixed(0)}) layers=$layers', + ); + } + } + if (!mounted) { + return; + } + setState(() { + _summary = + 'raw ${rawWidth.toStringAsFixed(0)}x' + '${rawHeight.toStringAsFixed(0)} layers: ' + '${_foundLayers.join(', ')}'; + }); + } +} diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index 83f3a54..d693ead 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -137,60 +137,6 @@ class _GiftPageState extends State with TickerProviderStateMixin { return 'type=${error.runtimeType} error=$error'; } - void _showLocalLuckyGiftFeedback( - List acceptUsers, { - required SocialChatGiftRes gift, - required int quantity, - required String giftBatchId, - int animationCount = 1, - }) { - final targetUserIds = _resolveAcceptUserIds(acceptUsers); - final rtmProvider = Provider.of( - navigatorKey.currentState!.context, - listen: false, - ); - final currentUser = AccountStorage().getCurrentUser()?.userProfile; - if (currentUser == null) { - _giftFxLog( - 'local lucky feedback skipped reason=no_current_user giftId=${gift.id}', - ); - return; - } - - for (final acceptUser in acceptUsers) { - final targetUser = acceptUser.user; - if (targetUser == null) { - _giftFxLog( - 'local lucky feedback skipped reason=no_target_user giftId=${gift.id}', - ); - continue; - } - _giftFxLog( - 'local lucky feedback trigger ' - 'giftId=${gift.id} ' - 'giftName=${gift.giftName} ' - 'toUserId=${targetUser.id} ' - 'toUserName=${targetUser.userNickname} ' - 'quantity=$quantity', - ); - rtmProvider.msgFloatingGiftListener?.call( - Msg( - groupId: - rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount, - msg: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", - type: SCRoomMsgType.luckGiftAnimOther, - gift: gift, - user: currentUser, - toUser: targetUser, - targetUserIds: targetUserIds, - giftBatchId: giftBatchId, - number: quantity, - customAnimationCount: animationCount, - ), - ); - } - } - void _applyGiftSelection(SocialChatGiftRes? gift, {bool notify = true}) { checkedGift = gift; if (gift != null && @@ -1602,13 +1548,6 @@ class _GiftPageState extends State with TickerProviderStateMixin { _showLocalCpInviteWaitingAfterGift(request, request.acceptUsers); if (request.isLuckyGiftRequest) { final giftBatchId = _createGiftBatchId(request); - _showLocalLuckyGiftFeedback( - request.acceptUsers, - gift: request.gift, - quantity: request.quantity, - giftBatchId: giftBatchId, - animationCount: request.clickCount, - ); await sendLuckGiftAnimOtherMsg( request.acceptUsers, gift: request.gift, @@ -2127,7 +2066,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { required String giftBatchId, int animationCount = 1, }) async { - final targetUserIds = _resolveAcceptUserIds(acceptUsers); + final targetUserIds = _resolveAcceptUserIdentityKeys(acceptUsers); final firstTargetUser = acceptUsers.isNotEmpty ? acceptUsers.first.user : null; _giftFxLog( @@ -2156,7 +2095,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { number: quantity, customAnimationCount: animationCount, type: SCRoomMsgType.luckGiftAnimOther, - msg: jsonEncode(acceptUsers.map((u) => u.user?.id).toList()), + msg: jsonEncode(targetUserIds), ), addLocal: true, ); @@ -2179,6 +2118,26 @@ class _GiftPageState extends State with TickerProviderStateMixin { return targetUserIds; } + List _resolveAcceptUserIdentityKeys(List acceptUsers) { + final targetUserIds = []; + void addTargetUserId(String? userId) { + final normalizedUserId = (userId ?? "").trim(); + if (normalizedUserId.isEmpty || + targetUserIds.contains(normalizedUserId)) { + return; + } + targetUserIds.add(normalizedUserId); + } + + for (final acceptUser in acceptUsers) { + final user = acceptUser.user; + addTargetUserId(user?.id); + addTargetUserId(user?.account); + addTargetUserId(user?.getID()); + } + return targetUserIds; + } + String _createGiftBatchId(_GiftSendRequest request) { final sortedAcceptUserIds = List.from(request.acceptUserIds) ..sort(); diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 6272115..5bd7fe6 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -175,6 +175,7 @@ class _VoiceRoomPageState extends State void _handleRoomRouteVisible() { _releaseCoveredRouteGuard(); _setRoomRouteVisible(true); + _scheduleEnsureRoomVisualEffectsEnabled(); } void _scheduleRoomRouteVisible() { @@ -249,13 +250,20 @@ class _VoiceRoomPageState extends State rtmProvider.msgFloatingGiftListener = _floatingGiftListener; rtmProvider.msgLuckyGiftRewardTickerListener = _luckyGiftRewardTickerListener; + debugPrint('[GiftFx][voice] bind room visual effect listeners'); } void _ensureRoomVisualEffectsEnabled() { final rtcProvider = Provider.of(context, listen: false); - if (rtcProvider.currenRoom == null || - !_roomRouteVisible || - rtcProvider.roomVisualEffectsEnabled) { + if (rtcProvider.currenRoom == null || !_roomRouteVisible) { + return; + } + final rtmProvider = Provider.of(context, listen: false); + final needsListenerRebind = + rtmProvider.msgFloatingGiftListener != _floatingGiftListener || + rtmProvider.msgLuckyGiftRewardTickerListener != + _luckyGiftRewardTickerListener; + if (rtcProvider.roomVisualEffectsEnabled && !needsListenerRebind) { return; } _enableRoomVisualEffects(); @@ -684,13 +692,36 @@ class _VoiceRoomPageState extends State _floatingGiftListener(Msg msg) { final rtcProvider = Provider.of(context, listen: false); if (!rtcProvider.shouldShowRoomVisualEffects) { + debugPrint( + '[GiftFx][voice] skip floating listener visual effects hidden ' + 'type=${msg.type} ' + 'giftId=${msg.gift?.id} ' + 'enabled=${rtcProvider.roomVisualEffectsEnabled} ' + 'visible=${rtcProvider.isVoiceRoomRouteVisible} ' + 'hasRoom=${rtcProvider.currenRoom != null}', + ); return; } if (Provider.of(context, listen: false).hideLGiftAnimal) { + debugPrint( + '[GiftFx][voice] skip floating listener hidden by gift preference ' + 'type=${msg.type} giftId=${msg.gift?.id}', + ); return; } final targetUserIds = _resolveGiftTargetUserIds(msg); final targetGroupKey = _buildGiftTargetGroupKey(targetUserIds); + debugPrint( + '[GiftFx][voice] floating listener recv ' + 'type=${msg.type} ' + 'giftId=${msg.gift?.id} ' + 'giftTab=${msg.gift?.giftTab} ' + 'quantity=${msg.number} ' + 'animationCount=${msg.customAnimationCount} ' + 'giftPhoto=${msg.gift?.giftPhoto} ' + 'targetUserIds=${targetUserIds.join(",")} ' + 'targetGroupKey=$targetGroupKey', + ); final shouldPlayBatchVisuals = targetUserIds.length <= 1 || targetGroupKey == null || @@ -726,7 +757,10 @@ class _VoiceRoomPageState extends State listen: false, ).enqueueGiftAnimation(giftModel); } else { - return; + debugPrint( + '[GiftFx][voice] skip gift ticker by visual batch dedupe ' + 'type=${msg.type} giftId=${msg.gift?.id} targetGroupKey=$targetGroupKey', + ); } final giftPhoto = (msg.gift?.giftPhoto ?? "").trim(); @@ -861,7 +895,9 @@ class _VoiceRoomPageState extends State bool _isLuckyGiftMessage(Msg msg) { final giftTab = (msg.gift?.giftTab ?? '').trim(); - return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name; + return giftTab == "LUCK" || + giftTab == SCGiftType.LUCKY_GIFT.name || + giftTab == SCGiftType.MAGIC.name; } bool _supportsComboMilestoneEffects(Msg msg) { @@ -956,26 +992,46 @@ class _VoiceRoomPageState extends State List targetUserIds, ) { if ((msg.number ?? 0) <= 0) { + debugPrint( + '[GiftFx][voice] skip lucky seat flight invalid quantity ' + 'giftId=${msg.gift?.id} quantity=${msg.number}', + ); return; } if (targetGroupKey == null || targetUserIds.isEmpty) { + debugPrint( + '[GiftFx][voice] skip lucky seat flight empty targets ' + 'giftId=${msg.gift?.id} targetGroupKey=$targetGroupKey', + ); return; } final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey); - final session = _luckyGiftComboSessions[sessionKey]; - if (session == null) { - return; - } + final session = _luckyGiftComboSessions.putIfAbsent( + sessionKey, + () => _LuckyGiftComboSession(), + ); session.endTimer?.cancel(); session.clearQueueTimer?.cancel(); - if (_shouldPlaySeatFlightGiftAnimation(msg) && giftPhoto.isNotEmpty) { + if (_shouldPlayLuckyGiftSeatFlightAnimation(msg) && giftPhoto.isNotEmpty) { final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds); if (targetUserIds.length > 1 && !_markGiftFlightBatchForPlayback(flightBatchKey)) { + debugPrint( + '[GiftFx][voice] skip lucky seat flight by flight batch dedupe ' + 'giftId=${msg.gift?.id} batchKey=$flightBatchKey', + ); _scheduleGiftAnimationSessionEnd(sessionKey); return; } + debugPrint( + '[GiftFx][voice] enqueue lucky seat flight ' + 'giftId=${msg.gift?.id} ' + 'giftPhoto=$giftPhoto ' + 'targetUserIds=${targetUserIds.join(",")} ' + 'animationCount=${_resolveTrackedAnimationCount(msg)} ' + 'batchKey=$flightBatchKey', + ); _enqueueTrackedSeatFlightAnimations( sessionKey: sessionKey, giftPhoto: giftPhoto, @@ -983,10 +1039,29 @@ class _VoiceRoomPageState extends State animationCount: _resolveTrackedAnimationCount(msg), batchKey: flightBatchKey, ); + } else { + debugPrint( + '[GiftFx][voice] skip lucky seat flight unsupported gift photo ' + 'giftId=${msg.gift?.id} giftPhoto=$giftPhoto', + ); } _scheduleGiftAnimationSessionEnd(sessionKey); } + bool _shouldPlayLuckyGiftSeatFlightAnimation(Msg msg) { + final gift = msg.gift; + if (gift == null) { + return false; + } + + final giftPhoto = (gift.giftPhoto ?? "").trim(); + if (giftPhoto.isEmpty) { + return false; + } + final giftPhotoExt = _normalizedGiftResourceExtension(giftPhoto); + return !_isAnimatedGiftResource(giftPhotoExt); + } + int _resolveTrackedAnimationCount(Msg msg) { return math.max(msg.customAnimationCount ?? 1, 1); } @@ -1016,21 +1091,51 @@ class _VoiceRoomPageState extends State ); final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds); if (normalizedTargetUserIds.isEmpty) { + debugPrint( + '[GiftFx][voice] skip seat flight enqueue empty normalized targets ' + 'sessionKey=$sessionKey', + ); return; } + final rtcProvider = Provider.of(context, listen: false); + final seenSeatKeys = >>{}; + final resolvedTargetUserIds = []; + final unresolvedTargetUserIds = []; + for (final targetUserId in normalizedTargetUserIds) { + final targetKey = rtcProvider.getSeatGlobalKeyByIndex(targetUserId); + if (targetKey == null) { + unresolvedTargetUserIds.add(targetUserId); + continue; + } + if (seenSeatKeys.add(targetKey)) { + resolvedTargetUserIds.add(targetUserId); + } + } + final flightTargetUserIds = + resolvedTargetUserIds.isNotEmpty + ? resolvedTargetUserIds + : normalizedTargetUserIds; + debugPrint( + '[GiftFx][voice] seat flight targets ' + 'sessionKey=$sessionKey ' + 'requested=${normalizedTargetUserIds.join(",")} ' + 'resolved=${resolvedTargetUserIds.join(",")} ' + 'unresolved=${unresolvedTargetUserIds.join(",")} ' + 'used=${flightTargetUserIds.join(",")}', + ); final normalizedAnimationCount = math.max(animationCount, 1); final cappedAnimationCount = math.min( normalizedAnimationCount, _maxTrackedGiftAnimations, ); final maxTrackedRequests = math.max( - _maxTrackedGiftAnimations * normalizedTargetUserIds.length, + _maxTrackedGiftAnimations * flightTargetUserIds.length, _maxTrackedGiftAnimations, ); for (var index = 0; index < cappedAnimationCount; index += 1) { final roundBatchTag = - normalizedTargetUserIds.length > 1 ? '$batchKey|round:$index' : null; - for (final targetUserId in normalizedTargetUserIds) { + flightTargetUserIds.length > 1 ? '$batchKey|round:$index' : null; + for (final targetUserId in flightTargetUserIds) { _giftSeatFlightController.enqueueLimited( RoomGiftSeatFlightRequest( imagePath: giftPhoto, diff --git a/lib/modules/room_game/views/baishun_game_page.dart b/lib/modules/room_game/views/baishun_game_page.dart index 6c2a1bb..6ef9b32 100644 --- a/lib/modules/room_game/views/baishun_game_page.dart +++ b/lib/modules/room_game/views/baishun_game_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/modules/room_game/bridge/baishun_js_bridge.dart'; import 'package:yumi/modules/room_game/data/models/room_game_models.dart'; @@ -50,6 +51,7 @@ class _BaishunGamePageState extends State { bool _didReceiveBridgeMessage = false; bool _didFinishPageLoad = false; bool _hasDeliveredLaunchConfig = false; + bool _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android; int _bridgeInjectCount = 0; String? _errorMessage; @@ -105,6 +107,7 @@ class _BaishunGamePageState extends State { onPageFinished: (String url) async { _didFinishPageLoad = true; _log('page_finished url=${_clip(url, 240)}'); + _showWebView(reason: 'page_finished'); await _injectBridge(reason: 'page_finished'); }, onWebResourceError: (WebResourceError error) { @@ -142,6 +145,7 @@ class _BaishunGamePageState extends State { _didReceiveBridgeMessage = false; _didFinishPageLoad = false; _hasDeliveredLaunchConfig = false; + _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android; _bridgeInjectCount = 0; _stopBridgeBootstrap(reason: 'prepare_page_load'); _bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), ( @@ -173,6 +177,7 @@ class _BaishunGamePageState extends State { return; } _log('loading_fallback_fire isLoading=$_isLoading'); + _showWebView(reason: 'loading_fallback'); setState(() { _isLoading = false; }); @@ -326,6 +331,7 @@ class _BaishunGamePageState extends State { return; } _log('game_loaded received'); + _showWebView(reason: 'game_loaded'); setState(() { _isLoading = false; _errorMessage = null; @@ -357,6 +363,16 @@ class _BaishunGamePageState extends State { } } + void _showWebView({required String reason}) { + if (_shouldMountWebView || !mounted) { + return; + } + _log('mount_webview reason=$reason'); + setState(() { + _shouldMountWebView = true; + }); + } + Future _reload() async { if (!mounted) { return; @@ -412,7 +428,11 @@ class _BaishunGamePageState extends State { return ''; } - void _log(String message) {} + void _log(String message) { + if (kDebugMode) { + debugPrint('$_logPrefix $message'); + } + } String _clip(String value, [int limit = 600]) { final trimmed = value.trim(); @@ -631,6 +651,26 @@ class _BaishunGamePageState extends State { return screenSize.width / ratio; } + Widget _buildWebView() { + final params = PlatformWebViewWidgetCreationParams( + controller: _controller.platform, + gestureRecognizers: _webGestureRecognizers, + ); + if (defaultTargetPlatform == TargetPlatform.android) { + return WebViewWidget.fromPlatformCreationParams( + params: + AndroidWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams( + params, + displayWithHybridComposition: true, + ), + ); + } + return WebViewWidget( + controller: _controller, + gestureRecognizers: _webGestureRecognizers, + ); + } + @override Widget build(BuildContext context) { final topCrop = 28.w; @@ -660,16 +700,14 @@ class _BaishunGamePageState extends State { color: const Color(0xFF081915), child: Stack( children: [ - Positioned( - top: -topCrop, - left: 0, - right: 0, - bottom: bottomFrameHeight, - child: WebViewWidget( - controller: _controller, - gestureRecognizers: _webGestureRecognizers, + if (_shouldMountWebView) + Positioned( + top: -topCrop, + left: 0, + right: 0, + bottom: bottomFrameHeight, + child: _buildWebView(), ), - ), if (bottomFrameHeight > 0) Positioned( left: 0, @@ -682,10 +720,12 @@ class _BaishunGamePageState extends State { ), if (_errorMessage != null) _buildErrorState(), if (_isLoading && _errorMessage == null) - const IgnorePointer( - ignoring: true, - child: BaishunLoadingView( - message: 'Waiting for gameLoaded...', + const Positioned.fill( + child: IgnorePointer( + ignoring: true, + child: BaishunLoadingView( + message: 'Waiting for gameLoaded...', + ), ), ), ], diff --git a/lib/modules/room_game/views/baishun_loading_view.dart b/lib/modules/room_game/views/baishun_loading_view.dart index cbf3984..2f14d61 100644 --- a/lib/modules/room_game/views/baishun_loading_view.dart +++ b/lib/modules/room_game/views/baishun_loading_view.dart @@ -14,7 +14,7 @@ class BaishunLoadingView extends StatelessWidget { @override Widget build(BuildContext context) { - return Positioned.fill( + return SizedBox.expand( child: Container( color: backgroundColor, alignment: Alignment.center, diff --git a/lib/modules/room_game/views/leader_game_page.dart b/lib/modules/room_game/views/leader_game_page.dart index 485aea6..d46237b 100644 --- a/lib/modules/room_game/views/leader_game_page.dart +++ b/lib/modules/room_game/views/leader_game_page.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/modules/room_game/bridge/leader_js_bridge.dart'; import 'package:yumi/modules/room_game/data/models/room_game_models.dart'; @@ -50,6 +51,7 @@ class _LeaderGamePageState extends State { bool _isClosing = false; bool _didReceiveBridgeMessage = false; bool _didFinishPageLoad = false; + bool _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android; int _bridgeInjectCount = 0; String? _errorMessage; @@ -105,6 +107,7 @@ class _LeaderGamePageState extends State { onPageFinished: (String url) async { _didFinishPageLoad = true; _log('page_finished url=${_clip(url, 240)}'); + _showWebView(reason: 'page_finished'); await _injectBridge(reason: 'page_finished'); }, onWebResourceError: (WebResourceError error) { @@ -136,6 +139,7 @@ class _LeaderGamePageState extends State { void _prepareForPageLoad() { _didReceiveBridgeMessage = false; _didFinishPageLoad = false; + _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android; _bridgeInjectCount = 0; _stopBridgeBootstrap(reason: 'prepare_page_load'); _bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), ( @@ -155,6 +159,7 @@ class _LeaderGamePageState extends State { return; } _log('loading_fallback_fire'); + _showWebView(reason: 'loading_fallback'); setState(() { _isLoading = false; }); @@ -274,6 +279,7 @@ class _LeaderGamePageState extends State { if (!mounted) { return; } + _showWebView(reason: 'load_complete'); setState(() { _isLoading = false; _errorMessage = null; @@ -294,6 +300,16 @@ class _LeaderGamePageState extends State { } } + void _showWebView({required String reason}) { + if (_shouldMountWebView || !mounted) { + return; + } + _log('mount_webview reason=$reason'); + setState(() { + _shouldMountWebView = true; + }); + } + Future _reload() async { if (!mounted) { return; @@ -385,7 +401,31 @@ class _LeaderGamePageState extends State { return fallback; } - void _log(String message) {} + Widget _buildWebView() { + final params = PlatformWebViewWidgetCreationParams( + controller: _controller.platform, + gestureRecognizers: _webGestureRecognizers, + ); + if (defaultTargetPlatform == TargetPlatform.android) { + return WebViewWidget.fromPlatformCreationParams( + params: + AndroidWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams( + params, + displayWithHybridComposition: true, + ), + ); + } + return WebViewWidget( + controller: _controller, + gestureRecognizers: _webGestureRecognizers, + ); + } + + void _log(String message) { + if (kDebugMode) { + debugPrint('$_logPrefix $message'); + } + } String _maskValue(String value, {int keepStart = 6, int keepEnd = 4}) { final trimmed = value.trim(); @@ -566,16 +606,14 @@ class _LeaderGamePageState extends State { color: const Color(0xFF081915), child: Stack( children: [ - Positioned( - left: 0, - right: 0, - top: 0, - bottom: bottomFrameHeight, - child: WebViewWidget( - controller: _controller, - gestureRecognizers: _webGestureRecognizers, + if (_shouldMountWebView) + Positioned( + left: 0, + right: 0, + top: 0, + bottom: bottomFrameHeight, + child: _buildWebView(), ), - ), if (bottomFrameHeight > 0) Positioned( left: 0, @@ -588,10 +626,12 @@ class _LeaderGamePageState extends State { ), if (_errorMessage != null) _buildErrorState(), if (_isLoading && _errorMessage == null) - const IgnorePointer( - ignoring: true, - child: BaishunLoadingView( - message: 'Waiting for Leader game...', + const Positioned.fill( + child: IgnorePointer( + ignoring: true, + child: BaishunLoadingView( + message: 'Waiting for Leader game...', + ), ), ), ], diff --git a/lib/modules/room_game/views/room_game_list_sheet.dart b/lib/modules/room_game/views/room_game_list_sheet.dart index 4961a61..9d3291a 100644 --- a/lib/modules/room_game/views/room_game_list_sheet.dart +++ b/lib/modules/room_game/views/room_game_list_sheet.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; @@ -192,7 +193,11 @@ class _RoomGameListSheetState extends State { } } - void _log(String message) {} + void _log(String message) { + if (kDebugMode) { + debugPrint('$_logPrefix $message'); + } + } String _clip(String value, [int limit = 600]) { final trimmed = value.trim(); diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 370ea38..5b381a7 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -51,6 +51,8 @@ import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart'; +import '../../shared/business_logic/models/res/sc_mic_go_up_res.dart' + hide PropsResources; import '../../shared/business_logic/models/res/sc_room_rocket_api_res.dart'; import '../../shared/business_logic/models/res/sc_room_red_packet_config_res.dart'; import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart'; @@ -523,13 +525,19 @@ class RealTimeCommunicationManager extends ChangeNotifier { Set _currentUserIdentityKeys() { final currentUser = AccountStorage().getCurrentUser()?.userProfile; - return {currentUser?.id?.trim() ?? "", currentUser?.account?.trim() ?? ""} - ..removeWhere((item) => item.isEmpty); + return { + currentUser?.id?.trim() ?? "", + currentUser?.account?.trim() ?? "", + currentUser?.getID().trim() ?? "", + }..removeWhere((item) => item.isEmpty); } Set _userProfileIdentityKeys(SocialChatUserProfile? user) { - return {user?.id?.trim() ?? "", user?.account?.trim() ?? ""} - ..removeWhere((item) => item.isEmpty); + return { + user?.id?.trim() ?? "", + user?.account?.trim() ?? "", + user?.getID().trim() ?? "", + }..removeWhere((item) => item.isEmpty); } bool _userProfileMatchesAnyIdentity( @@ -1564,8 +1572,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { } void _syncSelfMicRuntimeState() { - final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; - if ((currentUserId ?? "").isEmpty) { + final currentUserKeys = _currentUserIdentityKeys(); + if (currentUserKeys.isEmpty) { return; } final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; @@ -2417,6 +2425,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { bool keepAudioPublishing = false, }) async { if (!_roomRtcJoined || _roomRtcEngineAdapter == null) { + debugPrint( + '[RoomRtc] switchToBroadcaster skipped joined=$_roomRtcJoined ' + 'hasAdapter=${_roomRtcEngineAdapter != null}', + ); return false; } try { @@ -2425,6 +2437,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { RoomRtcAnchorConfig(muted: muted, publishMuted: localAudioMuted), ); if (!switched) { + debugPrint('[RoomRtc] switchToBroadcaster failed adapter=false'); _roomRtcRoleIsBroadcaster = false; _resyncHeartbeatFromRoomRtcState(); return false; @@ -2437,6 +2450,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { _resyncHeartbeatFromRoomRtcState(); return true; } catch (error) { + debugPrint('[RoomRtc] switchToBroadcaster error=$error'); _roomRtcRoleIsBroadcaster = false; _resyncHeartbeatFromRoomRtcState(); return false; @@ -5198,14 +5212,63 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (normalizedUserId.isEmpty) { return; } + final lookupKeys = {normalizedUserId}; + final currentUserKeys = _currentUserIdentityKeys(); + if (currentUserKeys.contains(normalizedUserId)) { + lookupKeys.addAll(currentUserKeys); + } roomWheatMap.forEach((seatIndex, seat) { - if (seat.user?.id != normalizedUserId || seatIndex == exceptIndex) { + final seatUserKeys = _userProfileIdentityKeys(seat.user); + if (!seatUserKeys.any(lookupKeys.contains) || seatIndex == exceptIndex) { return; } roomWheatMap[seatIndex] = seat.copyWith(clearUser: true); }); } + void _applySelfMicGoUpState({ + required SocialChatUserProfile myUser, + required num targetIndex, + required SCMicGoUpRes micGoUpRes, + required bool selfMicMuted, + }) { + final previousSelfSeatIndex = userOnMaiInIndex(myUser.id ?? ""); + final isSeatSwitching = + previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex; + + _clearSelfMicReleaseGuard(); + if (isSeatSwitching) { + _startSelfMicSwitchGuard( + sourceIndex: previousSelfSeatIndex, + targetIndex: targetIndex, + ); + } else { + _clearSelfMicSwitchGuard(); + _startSelfMicGoUpGuard(targetIndex: targetIndex); + } + _clearUserFromSeats(myUser.id, exceptIndex: targetIndex); + + final currentSeat = roomWheatMap[targetIndex]; + final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles); + roomWheatMap[targetIndex] = + currentSeat?.copyWith( + user: currentUser, + micMute: micGoUpRes.micMute, + micLock: micGoUpRes.micLock, + roomToken: micGoUpRes.roomToken, + ) ?? + MicRes( + roomId: currenRoom?.roomProfile?.roomProfile?.id, + micIndex: targetIndex, + micLock: micGoUpRes.micLock, + micMute: micGoUpRes.micMute, + user: currentUser, + roomToken: micGoUpRes.roomToken, + ); + isMic = selfMicMuted; + _syncSelfMicRuntimeState(); + } + void _openRoomUserInfoCard(String? userId) { final normalizedUserId = (userId ?? '').trim(); if (normalizedUserId.isEmpty) { @@ -5269,66 +5332,29 @@ class RealTimeCommunicationManager extends ChangeNotifier { final targetIndex = micGoUpRes.micIndex ?? index; final selfMicMuted = micGoUpRes.micMute ?? false; + if (myUser != null) { + _applySelfMicGoUpState( + myUser: myUser, + targetIndex: targetIndex, + micGoUpRes: micGoUpRes, + selfMicMuted: selfMicMuted, + ); + notifyListeners(); + } + final switched = await _switchRoomRtcToBroadcaster( muted: selfMicMuted, keepAudioPublishing: selfMicMuted && _currentUserPublishingRoomMusic, ); if (!switched) { - await _switchRoomRtcToAudience(); - try { - await SCChatRoomRepository().micGoDown( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - targetIndex, - ); - } catch (downError) {} - _clearSelfMicSwitchGuard(clearPreferredIndex: true); - _clearSelfMicGoUpGuard(clearPreferredIndex: true); - _clearUserFromSeats(myUser?.id); - _syncSelfMicRuntimeState(); - notifyListeners(); _refreshMicListSilently(notifyIfUnchanged: true); - _handleSelfMicGoUpFailure( - 'Failed to put on the microphone, TRTC switch failed', + _resetSelfMicGoUpFailureState(); + SCTts.show( + 'Microphone audio failed to start, please try switching microphone again', ); return; } - final previousSelfSeatIndex = - myUser != null ? userOnMaiInIndex(myUser.id ?? "") : -1; - final isSeatSwitching = - previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex; - if (myUser != null) { - _clearSelfMicReleaseGuard(); - if (isSeatSwitching) { - _startSelfMicSwitchGuard( - sourceIndex: previousSelfSeatIndex, - targetIndex: targetIndex, - ); - } else { - _clearSelfMicSwitchGuard(); - _startSelfMicGoUpGuard(targetIndex: targetIndex); - } - _clearUserFromSeats(myUser.id, exceptIndex: targetIndex); - final currentSeat = roomWheatMap[targetIndex]; - final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles); - roomWheatMap[targetIndex] = - currentSeat?.copyWith( - user: currentUser, - micMute: micGoUpRes.micMute, - micLock: micGoUpRes.micLock, - roomToken: micGoUpRes.roomToken, - ) ?? - MicRes( - roomId: currenRoom?.roomProfile?.roomProfile?.id, - micIndex: targetIndex, - micLock: micGoUpRes.micLock, - micMute: micGoUpRes.micMute, - user: currentUser, - roomToken: micGoUpRes.roomToken, - ); - } - isMic = selfMicMuted; - _syncSelfMicRuntimeState(); if (roomWheatMap[targetIndex]?.micMute ?? false) { ///房主上麦自动解禁麦位 if (isFz()) { @@ -5477,6 +5503,13 @@ class RealTimeCommunicationManager extends ChangeNotifier { ///点击的用户在哪个麦上 num userOnMaiInIndex(String userId) { + return _findUserOnMaiInIndex(userId, useDisplaySeatForCurrentUser: true); + } + + num _findUserOnMaiInIndex( + String userId, { + required bool useDisplaySeatForCurrentUser, + }) { final normalizedUserId = userId.trim(); if (normalizedUserId.isEmpty) { return -1; @@ -5484,12 +5517,17 @@ class RealTimeCommunicationManager extends ChangeNotifier { num index = -1; final currentUserKeys = _currentUserIdentityKeys(); final isCurrentUserQuery = currentUserKeys.contains(normalizedUserId); - final lookupKeys = - isCurrentUserQuery ? currentUserKeys : {normalizedUserId} - ..removeWhere((item) => item.isEmpty); + final lookupKeys = {normalizedUserId}; + if (isCurrentUserQuery) { + lookupKeys.addAll(currentUserKeys); + } + lookupKeys.removeWhere((item) => item.isEmpty); roomWheatMap.forEach((k, value) { - final visibleSeat = isCurrentUserQuery ? micAtIndexForDisplay(k) : value; - if (_userProfileMatchesAnyIdentity(visibleSeat?.user, lookupKeys)) { + final candidateSeat = + useDisplaySeatForCurrentUser && isCurrentUserQuery + ? micAtIndexForDisplay(k) + : value; + if (_userProfileMatchesAnyIdentity(candidateSeat?.user, lookupKeys)) { index = k; } }); @@ -5780,7 +5818,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { GlobalKey>? getSeatGlobalKeyByIndex(String userId) { ///需要有人的座位 - num index = userOnMaiInIndex(userId); + num index = _findUserOnMaiInIndex( + userId, + useDisplaySeatForCurrentUser: false, + ); if (index > -1) { return seatGlobalKeyMap[index]; } diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index e01ff20..97d8661 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -276,6 +276,33 @@ class RealTimeMessagingManager extends ChangeNotifier { ].join("|"); } + bool _shouldNotifyFloatingGiftListener(Msg msg) { + if (msg.type == SCRoomMsgType.gift) { + return true; + } + if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + return msg.user != null && msg.gift != null; + } + return false; + } + + void _notifyFloatingGiftListenerFromAddMsg(Msg msg) { + if (!_shouldNotifyFloatingGiftListener(msg)) { + return; + } + _giftFxLog( + 'trigger floating gift listener from addMsg ' + 'type=${msg.type} ' + 'giftId=${msg.gift?.id} ' + 'sendUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'quantity=${msg.number} ' + 'targetUserIds=${(msg.targetUserIds ?? const []).join(",")} ' + 'hasListener=${msgFloatingGiftListener != null}', + ); + msgFloatingGiftListener?.call(msg); + } + String _firstNonBlank(Iterable values) { for (final value in values) { final text = value?.trim(); @@ -2977,9 +3004,7 @@ class RealTimeMessagingManager extends ChangeNotifier { if (mergedGiftMsg != null) { msgAllListener?.call(mergedGiftMsg); msgGiftListener?.call(mergedGiftMsg); - if (msg.type == SCRoomMsgType.gift) { - msgFloatingGiftListener?.call(msg); - } + _notifyFloatingGiftListenerFromAddMsg(msg); notifyListeners(); return; } @@ -3071,12 +3096,41 @@ class RealTimeMessagingManager extends ChangeNotifier { roomGiftMsgList.removeAt(roomGiftMsgList.length - 1); } msgGiftListener?.call(msg); - if (msg.type == SCRoomMsgType.gift) { - msgFloatingGiftListener?.call(msg); - } + _notifyFloatingGiftListenerFromAddMsg(msg); } } + List _luckGiftTargetUserIdsFromMsg(Msg msg) { + final targetUserIds = []; + void addTargetUserId(dynamic userId) { + final normalizedUserId = userId?.toString().trim() ?? ''; + if (normalizedUserId.isEmpty || + targetUserIds.contains(normalizedUserId)) { + return; + } + targetUserIds.add(normalizedUserId); + } + + for (final userId in msg.targetUserIds ?? const []) { + addTargetUserId(userId); + } + + final rawTargetUserIds = (msg.msg ?? '').trim(); + if (rawTargetUserIds.startsWith('[')) { + try { + final decoded = jsonDecode(rawTargetUserIds); + if (decoded is List) { + for (final userId in decoded) { + addTargetUserId(userId); + } + } + } catch (_) {} + } + + addTargetUserId(msg.toUser?.id); + return targetUserIds; + } + void _enqueueCpRelationBroadcastFloatingIfNeeded( Msg msg, { bool isRegionBroadcast = false, @@ -6490,10 +6544,7 @@ class RealTimeMessagingManager extends ChangeNotifier { 'giftPhoto=${msg.gift?.giftPhoto}', ); } else { - final targetUserIds = - (jsonDecode(msg.msg ?? "") as List) - .map((e) => e as String) - .toList(); + final targetUserIds = _luckGiftTargetUserIdsFromMsg(msg); _giftFxLog( 'recv LUCK_GIFT_ANIM_OTHER ' 'giftPhoto=${msg.gift?.giftPhoto} ' @@ -6508,16 +6559,7 @@ class RealTimeMessagingManager extends ChangeNotifier { targetUserIds, ), ); - if (msg.user != null && msg.toUser != null && msg.gift != null) { - _giftFxLog( - 'trigger floating gift listener from LUCK_GIFT_ANIM_OTHER ' - 'sendUserId=${msg.user?.id} ' - 'toUserId=${msg.toUser?.id} ' - 'quantity=${msg.number} ' - 'giftId=${msg.gift?.id}', - ); - msgFloatingGiftListener?.call(msg); - } else { + if (!_shouldNotifyFloatingGiftListener(msg)) { _giftFxLog( 'skip floating gift listener from LUCK_GIFT_ANIM_OTHER ' 'reason=incomplete_msg ' diff --git a/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart b/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart index 7510870..5e49da8 100644 --- a/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart +++ b/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart @@ -5,6 +5,10 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; +void _seatFlightLog(String message) { + debugPrint('[GiftFx][seatFlight] $message'); +} + typedef RoomGiftSeatTargetResolver = GlobalKey>? Function(String targetUserId); @@ -52,8 +56,19 @@ class RoomGiftSeatFlightController { if (_state == null) { _trimPendingRequestsOverflow(); _pendingRequests.add(normalizedRequest); + _seatFlightLog( + 'buffer request no state ' + 'target=${normalizedRequest.targetUserId} ' + 'queueTag=${normalizedRequest.queueTag} ' + 'pending=${_pendingRequests.length}', + ); return; } + _seatFlightLog( + 'enqueue via attached state ' + 'target=${normalizedRequest.targetUserId} ' + 'queueTag=${normalizedRequest.queueTag}', + ); _state!._enqueue(normalizedRequest); } @@ -76,8 +91,20 @@ class RoomGiftSeatFlightController { _trimPendingRequestsForTag(queueTag, maxTrackedRequests); _trimPendingRequestsOverflow(); _pendingRequests.add(normalizedRequest); + _seatFlightLog( + 'buffer limited request no state ' + 'target=${normalizedRequest.targetUserId} ' + 'queueTag=$queueTag ' + 'pending=${_pendingRequests.length}', + ); return; } + _seatFlightLog( + 'enqueue limited via attached state ' + 'target=${normalizedRequest.targetUserId} ' + 'queueTag=$queueTag ' + 'max=$maxTrackedRequests', + ); _state!._enqueueLimited( normalizedRequest, maxTrackedRequests: maxTrackedRequests, @@ -184,6 +211,7 @@ class RoomGiftSeatFlightController { void _attach(_RoomGiftSeatFlightOverlayState state) { _state = state; + _seatFlightLog('attach overlay pending=${_pendingRequests.length}'); while (_pendingRequests.isNotEmpty) { state._enqueue(_pendingRequests.removeFirst()); } @@ -191,6 +219,7 @@ class RoomGiftSeatFlightController { void _detach(_RoomGiftSeatFlightOverlayState state) { if (_state == state) { + _seatFlightLog('detach overlay'); _state = null; } } @@ -262,6 +291,13 @@ class _RoomGiftSeatFlightOverlayState extends State _ensureCenterVisual(request); _trimQueuedRequestsOverflow(); _queue.add(_QueuedRoomGiftSeatFlightRequest(request: request)); + _seatFlightLog( + 'state enqueue ' + 'target=${request.targetUserId} ' + 'queue=${_queue.length} ' + 'queueTag=${request.queueTag} ' + 'batchTag=${request.batchTag}', + ); _syncIdleCenterVisual(); _scheduleNextAnimation(); } @@ -411,6 +447,10 @@ class _RoomGiftSeatFlightOverlayState extends State _queue.isEmpty || (expectedSessionToken != null && expectedSessionToken != _sessionToken)) { + _seatFlightLog( + 'skip start mounted=$mounted playing=$_isPlaying queue=${_queue.length} ' + 'expected=$expectedSessionToken session=$_sessionToken', + ); return; } @@ -441,9 +481,16 @@ class _RoomGiftSeatFlightOverlayState extends State ); } - final shouldRetryBatch = unresolvedBatch.any( - (queuedRequest) => queuedRequest.retryCount < 6, + _seatFlightLog( + 'start batch size=${queuedBatch.length} ' + 'active=${activeFlights.length} ' + 'unresolved=${unresolvedBatch.map((item) => item.request.targetUserId).join(",")} ' + 'queueRemaining=${_queue.length}', ); + + final shouldRetryBatch = + activeFlights.isEmpty && + unresolvedBatch.any((queuedRequest) => queuedRequest.retryCount < 6); if (shouldRetryBatch) { final retryBatch = queuedBatch @@ -466,6 +513,7 @@ class _RoomGiftSeatFlightOverlayState extends State } if (activeFlights.isEmpty) { + _seatFlightLog('drop batch no resolved targets'); _syncIdleCenterVisual(); _scheduleNextAnimation(); return; @@ -594,6 +642,13 @@ class _RoomGiftSeatFlightOverlayState extends State final targetKey = widget.resolveTargetKey(targetUserId); final targetContext = targetKey?.currentContext; if (overlayContext == null || targetContext == null) { + _seatFlightLog( + 'resolve target failed context ' + 'target=$targetUserId ' + 'hasOverlayContext=${overlayContext != null} ' + 'hasTargetKey=${targetKey != null} ' + 'hasTargetContext=${targetContext != null}', + ); return null; } @@ -603,6 +658,12 @@ class _RoomGiftSeatFlightOverlayState extends State targetBox is! RenderBox || !overlayBox.hasSize || !targetBox.hasSize) { + _seatFlightLog( + 'resolve target failed render ' + 'target=$targetUserId ' + 'overlayBox=${overlayBox.runtimeType} ' + 'targetBox=${targetBox.runtimeType}', + ); return null; } diff --git a/lib/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart index 50c1075..4cab6a6 100644 --- a/lib/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart @@ -14,6 +14,7 @@ class FloatingLuckyGiftGlobalScreenWidget extends StatefulWidget { super.key, required this.message, required this.onAnimationCompleted, + this.debugDisableAutoSlide = false, }); static const int floatingType = 7; @@ -24,6 +25,7 @@ class FloatingLuckyGiftGlobalScreenWidget extends StatefulWidget { final SCFloatingMessage message; final VoidCallback onAnimationCompleted; + final bool debugDisableAutoSlide; @override State createState() => @@ -73,7 +75,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState }); WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { + if (mounted && !widget.debugDisableAutoSlide) { _controller.forward(); } }); @@ -94,29 +96,19 @@ class _FloatingLuckyGiftGlobalScreenWidgetState onTap: _handleTap, onHorizontalDragEnd: _handleHorizontalDragEnd, child: SlideTransition( - position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, + position: + widget.debugDisableAutoSlide + ? const AlwaysStoppedAnimation(Offset.zero) + : (_isSwipeAnimating ? _swipeAnimation : _offsetAnimation), child: SizedBox( width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w, height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w, child: Stack( clipBehavior: Clip.none, children: [ + Positioned(left: 70.w, top: 34.w, child: _buildAvatar()), Positioned.fill(child: _buildBackground()), - Positioned(left: 30.w, top: 22.w, child: _buildAvatar()), - Positioned( - left: 88.w, - top: 26.w, - width: 160.w, - height: 45.w, - child: _buildTextBlock(), - ), - Positioned( - right: 23.w, - top: 24.w, - width: 61.w, - height: 45.w, - child: _buildMultiple(), - ), + Positioned.fill(child: _buildContentOverlay()), ], ), ), @@ -150,6 +142,28 @@ class _FloatingLuckyGiftGlobalScreenWidgetState ); } + Widget _buildContentOverlay() { + return Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: 110.w, + top: 24.w, + width: 120.w, + height: 46.w, + child: _buildTextBlock(), + ), + Positioned( + left: 220.w, + top: 24.w, + width: 70.w, + height: 46.w, + child: _buildMultiple(), + ), + ], + ); + } + Widget _buildAvatar() { const fallbackAsset = "sc_images/general/sc_icon_avar_defalt.png"; final avatarUrl = (widget.message.userAvatarUrl ?? "").trim(); @@ -157,37 +171,28 @@ class _FloatingLuckyGiftGlobalScreenWidgetState avatarUrl.isEmpty ? Image.asset( fallbackAsset, - width: 48.w, - height: 48.w, + width: 26.w, + height: 26.w, fit: BoxFit.cover, ) : Image( image: buildCachedImageProvider( avatarUrl, - logicalWidth: 48.w, - logicalHeight: 48.w, + logicalWidth: 38.w, + logicalHeight: 38.w, ), - width: 48.w, - height: 48.w, + width: 38.w, + height: 38.w, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Image.asset( fallbackAsset, - width: 48.w, - height: 48.w, + width: 38.w, + height: 38.w, fit: BoxFit.cover, ), ); - return Container( - width: 48.w, - height: 48.w, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: const Color(0xFFFFF4C2), width: 1.5.w), - ), - clipBehavior: Clip.antiAlias, - child: avatar, - ); + return ClipOval(child: avatar); } Widget _buildTextBlock() { @@ -201,20 +206,20 @@ class _FloatingLuckyGiftGlobalScreenWidgetState title, TextStyle( color: Colors.white, - fontSize: 19.sp, - fontWeight: FontWeight.w800, + fontSize: 14.sp, + fontWeight: FontWeight.w700, height: 1, decoration: TextDecoration.none, shadows: _textShadows(), ), ), - SizedBox(height: 7.w), + SizedBox(height: 1.w), _singleLineText( subtitle, TextStyle( color: Colors.white, - fontSize: 13.sp, - fontWeight: FontWeight.w700, + fontSize: 10.5.sp, + fontWeight: FontWeight.w600, height: 1, decoration: TextDecoration.none, shadows: _textShadows(), @@ -234,7 +239,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState textScaler: TextScaler.noScaling, style: TextStyle( color: Colors.white, - fontSize: 32.sp, + fontSize: 23.sp, fontWeight: FontWeight.w800, height: 1, decoration: TextDecoration.none, 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 442eaee..ac9fdf3 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 @@ -377,41 +377,42 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> { } final imageReplacementsByName = _imageReplacementsByName ?? const {}; - final useNativeAssetView = - assetName != null && Platform.isIOS && widget.loop; + final useNativeView = _shouldUseNativeRoomRocketPagView( + isIOS: Platform.isIOS, + hasAsset: assetName != null, + hasBytes: bytes != null, + ); final pagView = assetName != null ? PAGView.asset( assetName, key: _pagKey, - width: useNativeAssetView ? widget.width : null, - height: useNativeAssetView ? widget.height : null, - autoPlay: useNativeAssetView, - usePlatformView: useNativeAssetView, + width: useNativeView ? widget.width : null, + height: useNativeView ? widget.height : null, + autoPlay: useNativeView, + usePlatformView: useNativeView, repeatCount: repeatCount, - onInit: useNativeAssetView ? null : _handleInit, - onAnimationStart: - useNativeAssetView ? null : _handleAnimationStart, - onAnimationEnd: useNativeAssetView ? null : _handleAnimationEnd, - onAnimationCancel: - useNativeAssetView ? null : _handleAnimationCancel, - onAnimationRepeat: - useNativeAssetView ? null : _handleAnimationRepeat, + onInit: useNativeView ? null : _handleInit, + onAnimationStart: useNativeView ? null : _handleAnimationStart, + onAnimationEnd: useNativeView ? null : _handleAnimationEnd, + onAnimationCancel: useNativeView ? null : _handleAnimationCancel, + onAnimationRepeat: useNativeView ? null : _handleAnimationRepeat, defaultBuilder: (_) => const SizedBox(width: 1, height: 1), imageReplacementsByName: imageReplacementsByName, ) : PAGView.bytes( bytes, key: _pagKey, - width: null, - height: null, - autoPlay: false, + width: useNativeView ? widget.width : null, + height: useNativeView ? widget.height : null, + autoPlay: useNativeView, + usePlatformView: useNativeView, repeatCount: repeatCount, - onInit: _handleInit, - onAnimationStart: _handleAnimationStart, - onAnimationEnd: _handleAnimationEnd, - onAnimationCancel: _handleAnimationCancel, - onAnimationRepeat: _handleAnimationRepeat, + onInit: useNativeView ? null : _handleInit, + onAnimationStart: useNativeView ? null : _handleAnimationStart, + onAnimationEnd: useNativeView ? null : _handleAnimationEnd, + onAnimationCancel: useNativeView ? null : _handleAnimationCancel, + onAnimationRepeat: useNativeView ? null : _handleAnimationRepeat, defaultBuilder: (_) => const SizedBox(width: 1, height: 1), imageReplacementsByName: imageReplacementsByName, ); @@ -422,7 +423,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> { widget.clipBehavior == Clip.none ? Clip.none : Clip.hardEdge, children: [ if (visibleUnderlay != null) visibleUnderlay, - if (useNativeAssetView) + if (useNativeView) Positioned.fill(child: pagView) else _scaledPagView(pagView), @@ -475,7 +476,11 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> { urlsByName: imageReplacementUrls, ), ); - if (widget.loop) { + if (_shouldUseNativeRoomRocketPagView( + isIOS: Platform.isIOS, + hasAsset: true, + hasBytes: false, + )) { _startWatchdog?.cancel(); _initialized = true; } @@ -530,13 +535,25 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> { 'head=${_pagByteHead(bytes)} ' 'elapsedMs=${stopwatch.elapsedMilliseconds}', ); + final useNativeView = _shouldUseNativeRoomRocketPagView( + isIOS: Platform.isIOS, + hasAsset: false, + hasBytes: true, + ); setState(() { _pagBytes = bytes; _imageReplacementsByName = imageReplacements; _releasedFromMemory = false; + if (useNativeView) { + _initialized = true; + } }); _scheduleMemoryRelease(path); - _restartWatchdog(); + if (useNativeView) { + _startWatchdog?.cancel(); + } else { + _restartWatchdog(); + } } catch (error) { if (!_isCurrentResource(path, signature)) { return; @@ -907,6 +924,27 @@ String debugRoomRocketPagSourceKind(String resource) { return _pagSourceKind(resource); } +@visibleForTesting +bool debugShouldUseNativeRoomRocketPagView({ + required bool isIOS, + required bool hasAsset, + required bool hasBytes, +}) { + return _shouldUseNativeRoomRocketPagView( + isIOS: isIOS, + hasAsset: hasAsset, + hasBytes: hasBytes, + ); +} + +bool _shouldUseNativeRoomRocketPagView({ + required bool isIOS, + required bool hasAsset, + required bool hasBytes, +}) { + return isIOS && (hasAsset || hasBytes); +} + Future _readPagBytes(String value) async { final normalized = _normalizePagResource(value); final uri = _pagNetworkUri(normalized); diff --git a/lib/ui_kit/widgets/room/room_emoji_asset_image.dart b/lib/ui_kit/widgets/room/room_emoji_asset_image.dart index ab8561c..10c5280 100644 --- a/lib/ui_kit/widgets/room/room_emoji_asset_image.dart +++ b/lib/ui_kit/widgets/room/room_emoji_asset_image.dart @@ -1,14 +1,32 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:ui'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart'; const Duration _roomEmojiGifRestartInterval = Duration(milliseconds: 3500); +String _roomEmojiCacheKey(String resource, Object token) { + return 'room_emoji_${base64UrlEncode(utf8.encode('$resource|$token'))}'; +} + +int? _resolveRoomEmojiImageCacheDimension(double? logicalSize) { + if (logicalSize == null || !logicalSize.isFinite || logicalSize <= 0) { + return null; + } + final devicePixelRatio = + PlatformDispatcher.instance.implicitView?.devicePixelRatio ?? 2.0; + final maxDimension = SCGlobalConfig.isLowPerformanceDevice ? 1280 : 1920; + final pixelSize = (logicalSize * devicePixelRatio).round(); + return pixelSize.clamp(1, maxDimension).toInt(); +} + class RoomEmojiAssetImage extends StatefulWidget { const RoomEmojiAssetImage({ super.key, @@ -71,7 +89,7 @@ class _RoomEmojiAssetImageState extends State height: widget.height, loop: true, fit: widget.fit, - fallback: _buildNetworkFallback(resource, 'svga fallback'), + fallback: _buildNetworkFailure(resource, 'svga failed'), ), ); } @@ -135,6 +153,9 @@ class _RoomEmojiAssetImageState extends State headers: buildNetworkImageHeaders(normalizedUrl), cache: true, clearMemoryCacheWhenDispose: false, + clearMemoryCacheIfFailed: true, + cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width), + cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height), width: widget.width, height: widget.height, fit: widget.fit, @@ -166,23 +187,19 @@ class _RoomEmojiAssetImageState extends State 'failed error=$error', normalizedUrl, ); - return _buildNetworkFallback(normalizedUrl, 'image failed'); + return _buildNetworkFailure(normalizedUrl, 'image failed'); } }, ); } - Widget _buildNetworkFallback(String resource, String reason) { + Widget _buildNetworkFailure(String resource, String reason) { _logNetworkOnce( _loggedNetworkError, reason, normalizeImageResourceUrl(resource), ); - return SizedBox( - width: widget.width, - height: widget.height, - child: const Center(child: SCRotatingDotsLoading()), - ); + return SizedBox(width: widget.width, height: widget.height); } void _logNetworkOnce(Set bucket, String message, String url) { @@ -291,8 +308,11 @@ class _RestartingGifImageState extends State<_RestartingGifImage> { key: ValueKey('room_emoji_gif_placeholder_${normalizedUrl}_$token'), headers: buildNetworkImageHeaders(normalizedUrl), cache: true, - cacheKey: '$normalizedUrl#room_emoji_gif_${token % 2}', + cacheKey: _roomEmojiCacheKey(normalizedUrl, token % 2), clearMemoryCacheWhenDispose: false, + clearMemoryCacheIfFailed: true, + cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width), + cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height), width: widget.width, height: widget.height, fit: widget.fit, @@ -317,8 +337,11 @@ class _RestartingGifImageState extends State<_RestartingGifImage> { ), headers: buildNetworkImageHeaders(normalizedUrl), cache: true, - cacheKey: '$normalizedUrl#room_emoji_gif_${_restartToken % 2}', + cacheKey: _roomEmojiCacheKey(normalizedUrl, _restartToken % 2), clearMemoryCacheWhenDispose: true, + clearMemoryCacheIfFailed: true, + cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width), + cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height), width: widget.width, height: widget.height, fit: widget.fit, @@ -365,11 +388,7 @@ class _RestartingGifImageState extends State<_RestartingGifImage> { case LoadState.failed: final error = state.lastException ?? 'unknown'; _debugGifOnce(_loggedError, 'failed error=$error', normalizedUrl); - return SizedBox( - width: widget.width, - height: widget.height, - child: const Center(child: SCRotatingDotsLoading()), - ); + return SizedBox(width: widget.width, height: widget.height); } }, ); diff --git a/pubspec.lock b/pubspec.lock index 1659351..4e0297e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1781,7 +1781,7 @@ packages: source: hosted version: "4.4.2" webview_flutter_android: - dependency: transitive + dependency: "direct main" description: name: webview_flutter_android sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" diff --git a/pubspec.yaml b/pubspec.yaml index 9eabafb..a584edd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,10 +56,11 @@ dependencies: #路由 fluro: 2.0.5 #权限请求 - permission_handler: ^12.0.0+1 - webview_flutter: 4.4.2 - #跳转外链 - url_launcher: ^6.3.1 + permission_handler: ^12.0.0+1 + webview_flutter: 4.4.2 + webview_flutter_android: 3.16.9 + #跳转外链 + url_launcher: ^6.3.1 #键值对存储 # mmkv: ^2.2.2 shared_preferences: ^2.5.3 diff --git a/test/room_game_loading_view_test.dart b/test/room_game_loading_view_test.dart new file mode 100644 index 0000000..816ebbc --- /dev/null +++ b/test/room_game_loading_view_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/modules/room_game/views/baishun_loading_view.dart'; + +void main() { + testWidgets('room game loading overlay can be placed inside stack', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + ScreenUtilInit( + designSize: const Size(375, 812), + builder: + (_, __) => const MaterialApp( + home: Scaffold( + body: Stack( + children: [ + ColoredBox(color: Colors.black), + Positioned.fill( + child: IgnorePointer(child: BaishunLoadingView()), + ), + ], + ), + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + expect(find.byType(BaishunLoadingView), findsOneWidget); + }); +} diff --git a/test/room_rocket_pag_resource_test.dart b/test/room_rocket_pag_resource_test.dart index c1c4a3f..7c029e6 100644 --- a/test/room_rocket_pag_resource_test.dart +++ b/test/room_rocket_pag_resource_test.dart @@ -44,6 +44,33 @@ void main() { expect(debugRoomRocketPagSourceKind('assets/rocket/rocket.pag'), 'asset'); }); + test('uses native iOS pag view for loaded bytes and assets', () { + expect( + debugShouldUseNativeRoomRocketPagView( + isIOS: true, + hasAsset: false, + hasBytes: true, + ), + isTrue, + ); + expect( + debugShouldUseNativeRoomRocketPagView( + isIOS: true, + hasAsset: true, + hasBytes: false, + ), + isTrue, + ); + expect( + debugShouldUseNativeRoomRocketPagView( + isIOS: false, + hasAsset: true, + hasBytes: true, + ), + isFalse, + ); + }); + test('keeps pag bytes in memory for five minutes by default', () { expect(debugRoomRocketPagMemoryTtl(), const Duration(minutes: 5)); }); diff --git a/需求进度.md b/需求进度.md index 4c8539b..be04355 100644 --- a/需求进度.md +++ b/需求进度.md @@ -1055,3 +1055,4 @@ - [x] 2026-06-02 继续优化公屏 GIF 循环闪 loading:网络 GIF 重播时不再销毁整个 widget,加载下一轮期间复用上一帧画面,避免循环间隙出现转圈。 - [x] 2026-06-02 复核发现不销毁 key 会导致 GIF 解码器不重启并停在最后一帧;改为前景层用新 key 重播、背景层保留上一轮画面,兼顾循环播放和不显示 loading。 - [x] 2026-06-02 按接口文档恢复 `have` 权限判断:`/material/emoji/all` 返回的表情包仅 `have == true` 且非空时展示,`have=false/null` 计入 `filteredNoAccess` 日志。 +- [x] 2026-06-02 排查 Android 表情包一直转圈:初步判断不是接口解析,表情组已进入渲染后媒体加载失败态被转圈 fallback 掩盖;表情图片/GIF 按项目通用 `netImage` 补解码尺寸、失败清缓存,并把失败 fallback 改为静态占位便于区分 loading 与 failed。