自制飞屏动画处理
安卓待验证
This commit is contained in:
parent
122bc5668d
commit
732e2bfcfb
180
lib/debug/lucky_gift_global_floating_preview.dart
Normal file
180
lib/debug/lucky_gift_global_floating_preview.dart
Normal file
@ -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<PAGViewState>();
|
||||||
|
final _foundLayers = <String>{};
|
||||||
|
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<void> _scanLayers() async {
|
||||||
|
await Future<void>.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 =
|
||||||
|
<double>[
|
||||||
|
40,
|
||||||
|
80,
|
||||||
|
120,
|
||||||
|
170,
|
||||||
|
220,
|
||||||
|
280,
|
||||||
|
340,
|
||||||
|
410,
|
||||||
|
480,
|
||||||
|
560,
|
||||||
|
640,
|
||||||
|
710,
|
||||||
|
].where((value) => value < rawWidth).toList();
|
||||||
|
final sampleYs =
|
||||||
|
<double>[
|
||||||
|
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(', ')}';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -137,60 +137,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
return 'type=${error.runtimeType} error=$error';
|
return 'type=${error.runtimeType} error=$error';
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showLocalLuckyGiftFeedback(
|
|
||||||
List<MicRes> acceptUsers, {
|
|
||||||
required SocialChatGiftRes gift,
|
|
||||||
required int quantity,
|
|
||||||
required String giftBatchId,
|
|
||||||
int animationCount = 1,
|
|
||||||
}) {
|
|
||||||
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
|
||||||
final rtmProvider = Provider.of<RtmProvider>(
|
|
||||||
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}) {
|
void _applyGiftSelection(SocialChatGiftRes? gift, {bool notify = true}) {
|
||||||
checkedGift = gift;
|
checkedGift = gift;
|
||||||
if (gift != null &&
|
if (gift != null &&
|
||||||
@ -1602,13 +1548,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
_showLocalCpInviteWaitingAfterGift(request, request.acceptUsers);
|
_showLocalCpInviteWaitingAfterGift(request, request.acceptUsers);
|
||||||
if (request.isLuckyGiftRequest) {
|
if (request.isLuckyGiftRequest) {
|
||||||
final giftBatchId = _createGiftBatchId(request);
|
final giftBatchId = _createGiftBatchId(request);
|
||||||
_showLocalLuckyGiftFeedback(
|
|
||||||
request.acceptUsers,
|
|
||||||
gift: request.gift,
|
|
||||||
quantity: request.quantity,
|
|
||||||
giftBatchId: giftBatchId,
|
|
||||||
animationCount: request.clickCount,
|
|
||||||
);
|
|
||||||
await sendLuckGiftAnimOtherMsg(
|
await sendLuckGiftAnimOtherMsg(
|
||||||
request.acceptUsers,
|
request.acceptUsers,
|
||||||
gift: request.gift,
|
gift: request.gift,
|
||||||
@ -2127,7 +2066,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
required String giftBatchId,
|
required String giftBatchId,
|
||||||
int animationCount = 1,
|
int animationCount = 1,
|
||||||
}) async {
|
}) async {
|
||||||
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
final targetUserIds = _resolveAcceptUserIdentityKeys(acceptUsers);
|
||||||
final firstTargetUser =
|
final firstTargetUser =
|
||||||
acceptUsers.isNotEmpty ? acceptUsers.first.user : null;
|
acceptUsers.isNotEmpty ? acceptUsers.first.user : null;
|
||||||
_giftFxLog(
|
_giftFxLog(
|
||||||
@ -2156,7 +2095,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
number: quantity,
|
number: quantity,
|
||||||
customAnimationCount: animationCount,
|
customAnimationCount: animationCount,
|
||||||
type: SCRoomMsgType.luckGiftAnimOther,
|
type: SCRoomMsgType.luckGiftAnimOther,
|
||||||
msg: jsonEncode(acceptUsers.map((u) => u.user?.id).toList()),
|
msg: jsonEncode(targetUserIds),
|
||||||
),
|
),
|
||||||
addLocal: true,
|
addLocal: true,
|
||||||
);
|
);
|
||||||
@ -2179,6 +2118,26 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
return targetUserIds;
|
return targetUserIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> _resolveAcceptUserIdentityKeys(List<MicRes> acceptUsers) {
|
||||||
|
final targetUserIds = <String>[];
|
||||||
|
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) {
|
String _createGiftBatchId(_GiftSendRequest request) {
|
||||||
final sortedAcceptUserIds = List<String>.from(request.acceptUserIds)
|
final sortedAcceptUserIds = List<String>.from(request.acceptUserIds)
|
||||||
..sort();
|
..sort();
|
||||||
|
|||||||
@ -175,6 +175,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
void _handleRoomRouteVisible() {
|
void _handleRoomRouteVisible() {
|
||||||
_releaseCoveredRouteGuard();
|
_releaseCoveredRouteGuard();
|
||||||
_setRoomRouteVisible(true);
|
_setRoomRouteVisible(true);
|
||||||
|
_scheduleEnsureRoomVisualEffectsEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleRoomRouteVisible() {
|
void _scheduleRoomRouteVisible() {
|
||||||
@ -249,13 +250,20 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
rtmProvider.msgFloatingGiftListener = _floatingGiftListener;
|
rtmProvider.msgFloatingGiftListener = _floatingGiftListener;
|
||||||
rtmProvider.msgLuckyGiftRewardTickerListener =
|
rtmProvider.msgLuckyGiftRewardTickerListener =
|
||||||
_luckyGiftRewardTickerListener;
|
_luckyGiftRewardTickerListener;
|
||||||
|
debugPrint('[GiftFx][voice] bind room visual effect listeners');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _ensureRoomVisualEffectsEnabled() {
|
void _ensureRoomVisualEffectsEnabled() {
|
||||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||||
if (rtcProvider.currenRoom == null ||
|
if (rtcProvider.currenRoom == null || !_roomRouteVisible) {
|
||||||
!_roomRouteVisible ||
|
return;
|
||||||
rtcProvider.roomVisualEffectsEnabled) {
|
}
|
||||||
|
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||||
|
final needsListenerRebind =
|
||||||
|
rtmProvider.msgFloatingGiftListener != _floatingGiftListener ||
|
||||||
|
rtmProvider.msgLuckyGiftRewardTickerListener !=
|
||||||
|
_luckyGiftRewardTickerListener;
|
||||||
|
if (rtcProvider.roomVisualEffectsEnabled && !needsListenerRebind) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_enableRoomVisualEffects();
|
_enableRoomVisualEffects();
|
||||||
@ -684,13 +692,36 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
_floatingGiftListener(Msg msg) {
|
_floatingGiftListener(Msg msg) {
|
||||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||||
if (!rtcProvider.shouldShowRoomVisualEffects) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (Provider.of<GiftProvider>(context, listen: false).hideLGiftAnimal) {
|
if (Provider.of<GiftProvider>(context, listen: false).hideLGiftAnimal) {
|
||||||
|
debugPrint(
|
||||||
|
'[GiftFx][voice] skip floating listener hidden by gift preference '
|
||||||
|
'type=${msg.type} giftId=${msg.gift?.id}',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final targetUserIds = _resolveGiftTargetUserIds(msg);
|
final targetUserIds = _resolveGiftTargetUserIds(msg);
|
||||||
final targetGroupKey = _buildGiftTargetGroupKey(targetUserIds);
|
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 =
|
final shouldPlayBatchVisuals =
|
||||||
targetUserIds.length <= 1 ||
|
targetUserIds.length <= 1 ||
|
||||||
targetGroupKey == null ||
|
targetGroupKey == null ||
|
||||||
@ -726,7 +757,10 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
listen: false,
|
listen: false,
|
||||||
).enqueueGiftAnimation(giftModel);
|
).enqueueGiftAnimation(giftModel);
|
||||||
} else {
|
} 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();
|
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
|
||||||
@ -861,7 +895,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
|
|
||||||
bool _isLuckyGiftMessage(Msg msg) {
|
bool _isLuckyGiftMessage(Msg msg) {
|
||||||
final giftTab = (msg.gift?.giftTab ?? '').trim();
|
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) {
|
bool _supportsComboMilestoneEffects(Msg msg) {
|
||||||
@ -956,26 +992,46 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
List<String> targetUserIds,
|
List<String> targetUserIds,
|
||||||
) {
|
) {
|
||||||
if ((msg.number ?? 0) <= 0) {
|
if ((msg.number ?? 0) <= 0) {
|
||||||
|
debugPrint(
|
||||||
|
'[GiftFx][voice] skip lucky seat flight invalid quantity '
|
||||||
|
'giftId=${msg.gift?.id} quantity=${msg.number}',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (targetGroupKey == null || targetUserIds.isEmpty) {
|
if (targetGroupKey == null || targetUserIds.isEmpty) {
|
||||||
|
debugPrint(
|
||||||
|
'[GiftFx][voice] skip lucky seat flight empty targets '
|
||||||
|
'giftId=${msg.gift?.id} targetGroupKey=$targetGroupKey',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey);
|
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey);
|
||||||
final session = _luckyGiftComboSessions[sessionKey];
|
final session = _luckyGiftComboSessions.putIfAbsent(
|
||||||
if (session == null) {
|
sessionKey,
|
||||||
return;
|
() => _LuckyGiftComboSession(),
|
||||||
}
|
);
|
||||||
session.endTimer?.cancel();
|
session.endTimer?.cancel();
|
||||||
session.clearQueueTimer?.cancel();
|
session.clearQueueTimer?.cancel();
|
||||||
|
|
||||||
if (_shouldPlaySeatFlightGiftAnimation(msg) && giftPhoto.isNotEmpty) {
|
if (_shouldPlayLuckyGiftSeatFlightAnimation(msg) && giftPhoto.isNotEmpty) {
|
||||||
final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds);
|
final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds);
|
||||||
if (targetUserIds.length > 1 &&
|
if (targetUserIds.length > 1 &&
|
||||||
!_markGiftFlightBatchForPlayback(flightBatchKey)) {
|
!_markGiftFlightBatchForPlayback(flightBatchKey)) {
|
||||||
|
debugPrint(
|
||||||
|
'[GiftFx][voice] skip lucky seat flight by flight batch dedupe '
|
||||||
|
'giftId=${msg.gift?.id} batchKey=$flightBatchKey',
|
||||||
|
);
|
||||||
_scheduleGiftAnimationSessionEnd(sessionKey);
|
_scheduleGiftAnimationSessionEnd(sessionKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
debugPrint(
|
||||||
|
'[GiftFx][voice] enqueue lucky seat flight '
|
||||||
|
'giftId=${msg.gift?.id} '
|
||||||
|
'giftPhoto=$giftPhoto '
|
||||||
|
'targetUserIds=${targetUserIds.join(",")} '
|
||||||
|
'animationCount=${_resolveTrackedAnimationCount(msg)} '
|
||||||
|
'batchKey=$flightBatchKey',
|
||||||
|
);
|
||||||
_enqueueTrackedSeatFlightAnimations(
|
_enqueueTrackedSeatFlightAnimations(
|
||||||
sessionKey: sessionKey,
|
sessionKey: sessionKey,
|
||||||
giftPhoto: giftPhoto,
|
giftPhoto: giftPhoto,
|
||||||
@ -983,10 +1039,29 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
animationCount: _resolveTrackedAnimationCount(msg),
|
animationCount: _resolveTrackedAnimationCount(msg),
|
||||||
batchKey: flightBatchKey,
|
batchKey: flightBatchKey,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'[GiftFx][voice] skip lucky seat flight unsupported gift photo '
|
||||||
|
'giftId=${msg.gift?.id} giftPhoto=$giftPhoto',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_scheduleGiftAnimationSessionEnd(sessionKey);
|
_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) {
|
int _resolveTrackedAnimationCount(Msg msg) {
|
||||||
return math.max(msg.customAnimationCount ?? 1, 1);
|
return math.max(msg.customAnimationCount ?? 1, 1);
|
||||||
}
|
}
|
||||||
@ -1016,21 +1091,51 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
);
|
);
|
||||||
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
|
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
|
||||||
if (normalizedTargetUserIds.isEmpty) {
|
if (normalizedTargetUserIds.isEmpty) {
|
||||||
|
debugPrint(
|
||||||
|
'[GiftFx][voice] skip seat flight enqueue empty normalized targets '
|
||||||
|
'sessionKey=$sessionKey',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||||
|
final seenSeatKeys = <GlobalKey<State<StatefulWidget>>>{};
|
||||||
|
final resolvedTargetUserIds = <String>[];
|
||||||
|
final unresolvedTargetUserIds = <String>[];
|
||||||
|
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 normalizedAnimationCount = math.max(animationCount, 1);
|
||||||
final cappedAnimationCount = math.min(
|
final cappedAnimationCount = math.min(
|
||||||
normalizedAnimationCount,
|
normalizedAnimationCount,
|
||||||
_maxTrackedGiftAnimations,
|
_maxTrackedGiftAnimations,
|
||||||
);
|
);
|
||||||
final maxTrackedRequests = math.max(
|
final maxTrackedRequests = math.max(
|
||||||
_maxTrackedGiftAnimations * normalizedTargetUserIds.length,
|
_maxTrackedGiftAnimations * flightTargetUserIds.length,
|
||||||
_maxTrackedGiftAnimations,
|
_maxTrackedGiftAnimations,
|
||||||
);
|
);
|
||||||
for (var index = 0; index < cappedAnimationCount; index += 1) {
|
for (var index = 0; index < cappedAnimationCount; index += 1) {
|
||||||
final roundBatchTag =
|
final roundBatchTag =
|
||||||
normalizedTargetUserIds.length > 1 ? '$batchKey|round:$index' : null;
|
flightTargetUserIds.length > 1 ? '$batchKey|round:$index' : null;
|
||||||
for (final targetUserId in normalizedTargetUserIds) {
|
for (final targetUserId in flightTargetUserIds) {
|
||||||
_giftSeatFlightController.enqueueLimited(
|
_giftSeatFlightController.enqueueLimited(
|
||||||
RoomGiftSeatFlightRequest(
|
RoomGiftSeatFlightRequest(
|
||||||
imagePath: giftPhoto,
|
imagePath: giftPhoto,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.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/app/routes/sc_fluro_navigator.dart';
|
||||||
import 'package:yumi/modules/room_game/bridge/baishun_js_bridge.dart';
|
import 'package:yumi/modules/room_game/bridge/baishun_js_bridge.dart';
|
||||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||||
@ -50,6 +51,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
bool _didReceiveBridgeMessage = false;
|
bool _didReceiveBridgeMessage = false;
|
||||||
bool _didFinishPageLoad = false;
|
bool _didFinishPageLoad = false;
|
||||||
bool _hasDeliveredLaunchConfig = false;
|
bool _hasDeliveredLaunchConfig = false;
|
||||||
|
bool _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
|
||||||
int _bridgeInjectCount = 0;
|
int _bridgeInjectCount = 0;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
|
||||||
@ -105,6 +107,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
onPageFinished: (String url) async {
|
onPageFinished: (String url) async {
|
||||||
_didFinishPageLoad = true;
|
_didFinishPageLoad = true;
|
||||||
_log('page_finished url=${_clip(url, 240)}');
|
_log('page_finished url=${_clip(url, 240)}');
|
||||||
|
_showWebView(reason: 'page_finished');
|
||||||
await _injectBridge(reason: 'page_finished');
|
await _injectBridge(reason: 'page_finished');
|
||||||
},
|
},
|
||||||
onWebResourceError: (WebResourceError error) {
|
onWebResourceError: (WebResourceError error) {
|
||||||
@ -142,6 +145,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
_didReceiveBridgeMessage = false;
|
_didReceiveBridgeMessage = false;
|
||||||
_didFinishPageLoad = false;
|
_didFinishPageLoad = false;
|
||||||
_hasDeliveredLaunchConfig = false;
|
_hasDeliveredLaunchConfig = false;
|
||||||
|
_shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
|
||||||
_bridgeInjectCount = 0;
|
_bridgeInjectCount = 0;
|
||||||
_stopBridgeBootstrap(reason: 'prepare_page_load');
|
_stopBridgeBootstrap(reason: 'prepare_page_load');
|
||||||
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
|
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
|
||||||
@ -173,6 +177,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log('loading_fallback_fire isLoading=$_isLoading');
|
_log('loading_fallback_fire isLoading=$_isLoading');
|
||||||
|
_showWebView(reason: 'loading_fallback');
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
@ -326,6 +331,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log('game_loaded received');
|
_log('game_loaded received');
|
||||||
|
_showWebView(reason: 'game_loaded');
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@ -357,6 +363,16 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showWebView({required String reason}) {
|
||||||
|
if (_shouldMountWebView || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log('mount_webview reason=$reason');
|
||||||
|
setState(() {
|
||||||
|
_shouldMountWebView = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _reload() async {
|
Future<void> _reload() async {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@ -412,7 +428,11 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void _log(String message) {}
|
void _log(String message) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('$_logPrefix $message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String _clip(String value, [int limit = 600]) {
|
String _clip(String value, [int limit = 600]) {
|
||||||
final trimmed = value.trim();
|
final trimmed = value.trim();
|
||||||
@ -631,6 +651,26 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
return screenSize.width / ratio;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final topCrop = 28.w;
|
final topCrop = 28.w;
|
||||||
@ -660,16 +700,14 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
color: const Color(0xFF081915),
|
color: const Color(0xFF081915),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
if (_shouldMountWebView)
|
||||||
top: -topCrop,
|
Positioned(
|
||||||
left: 0,
|
top: -topCrop,
|
||||||
right: 0,
|
left: 0,
|
||||||
bottom: bottomFrameHeight,
|
right: 0,
|
||||||
child: WebViewWidget(
|
bottom: bottomFrameHeight,
|
||||||
controller: _controller,
|
child: _buildWebView(),
|
||||||
gestureRecognizers: _webGestureRecognizers,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (bottomFrameHeight > 0)
|
if (bottomFrameHeight > 0)
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -682,10 +720,12 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
|||||||
),
|
),
|
||||||
if (_errorMessage != null) _buildErrorState(),
|
if (_errorMessage != null) _buildErrorState(),
|
||||||
if (_isLoading && _errorMessage == null)
|
if (_isLoading && _errorMessage == null)
|
||||||
const IgnorePointer(
|
const Positioned.fill(
|
||||||
ignoring: true,
|
child: IgnorePointer(
|
||||||
child: BaishunLoadingView(
|
ignoring: true,
|
||||||
message: 'Waiting for gameLoaded...',
|
child: BaishunLoadingView(
|
||||||
|
message: 'Waiting for gameLoaded...',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class BaishunLoadingView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Positioned.fill(
|
return SizedBox.expand(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.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/app/routes/sc_fluro_navigator.dart';
|
||||||
import 'package:yumi/modules/room_game/bridge/leader_js_bridge.dart';
|
import 'package:yumi/modules/room_game/bridge/leader_js_bridge.dart';
|
||||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||||
@ -50,6 +51,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
|||||||
bool _isClosing = false;
|
bool _isClosing = false;
|
||||||
bool _didReceiveBridgeMessage = false;
|
bool _didReceiveBridgeMessage = false;
|
||||||
bool _didFinishPageLoad = false;
|
bool _didFinishPageLoad = false;
|
||||||
|
bool _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
|
||||||
int _bridgeInjectCount = 0;
|
int _bridgeInjectCount = 0;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
|
||||||
@ -105,6 +107,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
|||||||
onPageFinished: (String url) async {
|
onPageFinished: (String url) async {
|
||||||
_didFinishPageLoad = true;
|
_didFinishPageLoad = true;
|
||||||
_log('page_finished url=${_clip(url, 240)}');
|
_log('page_finished url=${_clip(url, 240)}');
|
||||||
|
_showWebView(reason: 'page_finished');
|
||||||
await _injectBridge(reason: 'page_finished');
|
await _injectBridge(reason: 'page_finished');
|
||||||
},
|
},
|
||||||
onWebResourceError: (WebResourceError error) {
|
onWebResourceError: (WebResourceError error) {
|
||||||
@ -136,6 +139,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
|||||||
void _prepareForPageLoad() {
|
void _prepareForPageLoad() {
|
||||||
_didReceiveBridgeMessage = false;
|
_didReceiveBridgeMessage = false;
|
||||||
_didFinishPageLoad = false;
|
_didFinishPageLoad = false;
|
||||||
|
_shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
|
||||||
_bridgeInjectCount = 0;
|
_bridgeInjectCount = 0;
|
||||||
_stopBridgeBootstrap(reason: 'prepare_page_load');
|
_stopBridgeBootstrap(reason: 'prepare_page_load');
|
||||||
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
|
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
|
||||||
@ -155,6 +159,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log('loading_fallback_fire');
|
_log('loading_fallback_fire');
|
||||||
|
_showWebView(reason: 'loading_fallback');
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
@ -274,6 +279,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_showWebView(reason: 'load_complete');
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@ -294,6 +300,16 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showWebView({required String reason}) {
|
||||||
|
if (_shouldMountWebView || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log('mount_webview reason=$reason');
|
||||||
|
setState(() {
|
||||||
|
_shouldMountWebView = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _reload() async {
|
Future<void> _reload() async {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@ -385,7 +401,31 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
|||||||
return fallback;
|
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}) {
|
String _maskValue(String value, {int keepStart = 6, int keepEnd = 4}) {
|
||||||
final trimmed = value.trim();
|
final trimmed = value.trim();
|
||||||
@ -566,16 +606,14 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
|||||||
color: const Color(0xFF081915),
|
color: const Color(0xFF081915),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
if (_shouldMountWebView)
|
||||||
left: 0,
|
Positioned(
|
||||||
right: 0,
|
left: 0,
|
||||||
top: 0,
|
right: 0,
|
||||||
bottom: bottomFrameHeight,
|
top: 0,
|
||||||
child: WebViewWidget(
|
bottom: bottomFrameHeight,
|
||||||
controller: _controller,
|
child: _buildWebView(),
|
||||||
gestureRecognizers: _webGestureRecognizers,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (bottomFrameHeight > 0)
|
if (bottomFrameHeight > 0)
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -588,10 +626,12 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
|||||||
),
|
),
|
||||||
if (_errorMessage != null) _buildErrorState(),
|
if (_errorMessage != null) _buildErrorState(),
|
||||||
if (_isLoading && _errorMessage == null)
|
if (_isLoading && _errorMessage == null)
|
||||||
const IgnorePointer(
|
const Positioned.fill(
|
||||||
ignoring: true,
|
child: IgnorePointer(
|
||||||
child: BaishunLoadingView(
|
ignoring: true,
|
||||||
message: 'Waiting for Leader game...',
|
child: BaishunLoadingView(
|
||||||
|
message: 'Waiting for Leader game...',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -192,7 +193,11 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _log(String message) {}
|
void _log(String message) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('$_logPrefix $message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String _clip(String value, [int limit = 600]) {
|
String _clip(String value, [int limit = 600]) {
|
||||||
final trimmed = value.trim();
|
final trimmed = value.trim();
|
||||||
|
|||||||
@ -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_info_event_type.dart';
|
||||||
import '../../shared/data_sources/models/enum/sc_room_roles_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_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_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_config_res.dart';
|
||||||
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||||||
@ -523,13 +525,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
|
|
||||||
Set<String> _currentUserIdentityKeys() {
|
Set<String> _currentUserIdentityKeys() {
|
||||||
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
|
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
|
||||||
return {currentUser?.id?.trim() ?? "", currentUser?.account?.trim() ?? ""}
|
return {
|
||||||
..removeWhere((item) => item.isEmpty);
|
currentUser?.id?.trim() ?? "",
|
||||||
|
currentUser?.account?.trim() ?? "",
|
||||||
|
currentUser?.getID().trim() ?? "",
|
||||||
|
}..removeWhere((item) => item.isEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> _userProfileIdentityKeys(SocialChatUserProfile? user) {
|
Set<String> _userProfileIdentityKeys(SocialChatUserProfile? user) {
|
||||||
return {user?.id?.trim() ?? "", user?.account?.trim() ?? ""}
|
return {
|
||||||
..removeWhere((item) => item.isEmpty);
|
user?.id?.trim() ?? "",
|
||||||
|
user?.account?.trim() ?? "",
|
||||||
|
user?.getID().trim() ?? "",
|
||||||
|
}..removeWhere((item) => item.isEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _userProfileMatchesAnyIdentity(
|
bool _userProfileMatchesAnyIdentity(
|
||||||
@ -1564,8 +1572,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _syncSelfMicRuntimeState() {
|
void _syncSelfMicRuntimeState() {
|
||||||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
final currentUserKeys = _currentUserIdentityKeys();
|
||||||
if ((currentUserId ?? "").isEmpty) {
|
if (currentUserKeys.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||||
@ -2417,6 +2425,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
bool keepAudioPublishing = false,
|
bool keepAudioPublishing = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (!_roomRtcJoined || _roomRtcEngineAdapter == null) {
|
if (!_roomRtcJoined || _roomRtcEngineAdapter == null) {
|
||||||
|
debugPrint(
|
||||||
|
'[RoomRtc] switchToBroadcaster skipped joined=$_roomRtcJoined '
|
||||||
|
'hasAdapter=${_roomRtcEngineAdapter != null}',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -2425,6 +2437,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
RoomRtcAnchorConfig(muted: muted, publishMuted: localAudioMuted),
|
RoomRtcAnchorConfig(muted: muted, publishMuted: localAudioMuted),
|
||||||
);
|
);
|
||||||
if (!switched) {
|
if (!switched) {
|
||||||
|
debugPrint('[RoomRtc] switchToBroadcaster failed adapter=false');
|
||||||
_roomRtcRoleIsBroadcaster = false;
|
_roomRtcRoleIsBroadcaster = false;
|
||||||
_resyncHeartbeatFromRoomRtcState();
|
_resyncHeartbeatFromRoomRtcState();
|
||||||
return false;
|
return false;
|
||||||
@ -2437,6 +2450,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
_resyncHeartbeatFromRoomRtcState();
|
_resyncHeartbeatFromRoomRtcState();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
debugPrint('[RoomRtc] switchToBroadcaster error=$error');
|
||||||
_roomRtcRoleIsBroadcaster = false;
|
_roomRtcRoleIsBroadcaster = false;
|
||||||
_resyncHeartbeatFromRoomRtcState();
|
_resyncHeartbeatFromRoomRtcState();
|
||||||
return false;
|
return false;
|
||||||
@ -5198,14 +5212,63 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
if (normalizedUserId.isEmpty) {
|
if (normalizedUserId.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final lookupKeys = <String>{normalizedUserId};
|
||||||
|
final currentUserKeys = _currentUserIdentityKeys();
|
||||||
|
if (currentUserKeys.contains(normalizedUserId)) {
|
||||||
|
lookupKeys.addAll(currentUserKeys);
|
||||||
|
}
|
||||||
roomWheatMap.forEach((seatIndex, seat) {
|
roomWheatMap.forEach((seatIndex, seat) {
|
||||||
if (seat.user?.id != normalizedUserId || seatIndex == exceptIndex) {
|
final seatUserKeys = _userProfileIdentityKeys(seat.user);
|
||||||
|
if (!seatUserKeys.any(lookupKeys.contains) || seatIndex == exceptIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
roomWheatMap[seatIndex] = seat.copyWith(clearUser: true);
|
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) {
|
void _openRoomUserInfoCard(String? userId) {
|
||||||
final normalizedUserId = (userId ?? '').trim();
|
final normalizedUserId = (userId ?? '').trim();
|
||||||
if (normalizedUserId.isEmpty) {
|
if (normalizedUserId.isEmpty) {
|
||||||
@ -5269,66 +5332,29 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
|
|
||||||
final targetIndex = micGoUpRes.micIndex ?? index;
|
final targetIndex = micGoUpRes.micIndex ?? index;
|
||||||
final selfMicMuted = micGoUpRes.micMute ?? false;
|
final selfMicMuted = micGoUpRes.micMute ?? false;
|
||||||
|
if (myUser != null) {
|
||||||
|
_applySelfMicGoUpState(
|
||||||
|
myUser: myUser,
|
||||||
|
targetIndex: targetIndex,
|
||||||
|
micGoUpRes: micGoUpRes,
|
||||||
|
selfMicMuted: selfMicMuted,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
final switched = await _switchRoomRtcToBroadcaster(
|
final switched = await _switchRoomRtcToBroadcaster(
|
||||||
muted: selfMicMuted,
|
muted: selfMicMuted,
|
||||||
keepAudioPublishing: selfMicMuted && _currentUserPublishingRoomMusic,
|
keepAudioPublishing: selfMicMuted && _currentUserPublishingRoomMusic,
|
||||||
);
|
);
|
||||||
if (!switched) {
|
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);
|
_refreshMicListSilently(notifyIfUnchanged: true);
|
||||||
_handleSelfMicGoUpFailure(
|
_resetSelfMicGoUpFailureState();
|
||||||
'Failed to put on the microphone, TRTC switch failed',
|
SCTts.show(
|
||||||
|
'Microphone audio failed to start, please try switching microphone again',
|
||||||
);
|
);
|
||||||
return;
|
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 (roomWheatMap[targetIndex]?.micMute ?? false) {
|
||||||
///房主上麦自动解禁麦位
|
///房主上麦自动解禁麦位
|
||||||
if (isFz()) {
|
if (isFz()) {
|
||||||
@ -5477,6 +5503,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
|
|
||||||
///点击的用户在哪个麦上
|
///点击的用户在哪个麦上
|
||||||
num userOnMaiInIndex(String userId) {
|
num userOnMaiInIndex(String userId) {
|
||||||
|
return _findUserOnMaiInIndex(userId, useDisplaySeatForCurrentUser: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
num _findUserOnMaiInIndex(
|
||||||
|
String userId, {
|
||||||
|
required bool useDisplaySeatForCurrentUser,
|
||||||
|
}) {
|
||||||
final normalizedUserId = userId.trim();
|
final normalizedUserId = userId.trim();
|
||||||
if (normalizedUserId.isEmpty) {
|
if (normalizedUserId.isEmpty) {
|
||||||
return -1;
|
return -1;
|
||||||
@ -5484,12 +5517,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
num index = -1;
|
num index = -1;
|
||||||
final currentUserKeys = _currentUserIdentityKeys();
|
final currentUserKeys = _currentUserIdentityKeys();
|
||||||
final isCurrentUserQuery = currentUserKeys.contains(normalizedUserId);
|
final isCurrentUserQuery = currentUserKeys.contains(normalizedUserId);
|
||||||
final lookupKeys =
|
final lookupKeys = <String>{normalizedUserId};
|
||||||
isCurrentUserQuery ? currentUserKeys : <String>{normalizedUserId}
|
if (isCurrentUserQuery) {
|
||||||
..removeWhere((item) => item.isEmpty);
|
lookupKeys.addAll(currentUserKeys);
|
||||||
|
}
|
||||||
|
lookupKeys.removeWhere((item) => item.isEmpty);
|
||||||
roomWheatMap.forEach((k, value) {
|
roomWheatMap.forEach((k, value) {
|
||||||
final visibleSeat = isCurrentUserQuery ? micAtIndexForDisplay(k) : value;
|
final candidateSeat =
|
||||||
if (_userProfileMatchesAnyIdentity(visibleSeat?.user, lookupKeys)) {
|
useDisplaySeatForCurrentUser && isCurrentUserQuery
|
||||||
|
? micAtIndexForDisplay(k)
|
||||||
|
: value;
|
||||||
|
if (_userProfileMatchesAnyIdentity(candidateSeat?.user, lookupKeys)) {
|
||||||
index = k;
|
index = k;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -5780,7 +5818,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
|
|
||||||
GlobalKey<State<StatefulWidget>>? getSeatGlobalKeyByIndex(String userId) {
|
GlobalKey<State<StatefulWidget>>? getSeatGlobalKeyByIndex(String userId) {
|
||||||
///需要有人的座位
|
///需要有人的座位
|
||||||
num index = userOnMaiInIndex(userId);
|
num index = _findUserOnMaiInIndex(
|
||||||
|
userId,
|
||||||
|
useDisplaySeatForCurrentUser: false,
|
||||||
|
);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
return seatGlobalKeyMap[index];
|
return seatGlobalKeyMap[index];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -276,6 +276,33 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
].join("|");
|
].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 <String>[]).join(",")} '
|
||||||
|
'hasListener=${msgFloatingGiftListener != null}',
|
||||||
|
);
|
||||||
|
msgFloatingGiftListener?.call(msg);
|
||||||
|
}
|
||||||
|
|
||||||
String _firstNonBlank(Iterable<String?> values) {
|
String _firstNonBlank(Iterable<String?> values) {
|
||||||
for (final value in values) {
|
for (final value in values) {
|
||||||
final text = value?.trim();
|
final text = value?.trim();
|
||||||
@ -2977,9 +3004,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
if (mergedGiftMsg != null) {
|
if (mergedGiftMsg != null) {
|
||||||
msgAllListener?.call(mergedGiftMsg);
|
msgAllListener?.call(mergedGiftMsg);
|
||||||
msgGiftListener?.call(mergedGiftMsg);
|
msgGiftListener?.call(mergedGiftMsg);
|
||||||
if (msg.type == SCRoomMsgType.gift) {
|
_notifyFloatingGiftListenerFromAddMsg(msg);
|
||||||
msgFloatingGiftListener?.call(msg);
|
|
||||||
}
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3071,12 +3096,41 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
roomGiftMsgList.removeAt(roomGiftMsgList.length - 1);
|
roomGiftMsgList.removeAt(roomGiftMsgList.length - 1);
|
||||||
}
|
}
|
||||||
msgGiftListener?.call(msg);
|
msgGiftListener?.call(msg);
|
||||||
if (msg.type == SCRoomMsgType.gift) {
|
_notifyFloatingGiftListenerFromAddMsg(msg);
|
||||||
msgFloatingGiftListener?.call(msg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> _luckGiftTargetUserIdsFromMsg(Msg msg) {
|
||||||
|
final targetUserIds = <String>[];
|
||||||
|
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 <String>[]) {
|
||||||
|
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(
|
void _enqueueCpRelationBroadcastFloatingIfNeeded(
|
||||||
Msg msg, {
|
Msg msg, {
|
||||||
bool isRegionBroadcast = false,
|
bool isRegionBroadcast = false,
|
||||||
@ -6490,10 +6544,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
'giftPhoto=${msg.gift?.giftPhoto}',
|
'giftPhoto=${msg.gift?.giftPhoto}',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final targetUserIds =
|
final targetUserIds = _luckGiftTargetUserIdsFromMsg(msg);
|
||||||
(jsonDecode(msg.msg ?? "") as List)
|
|
||||||
.map((e) => e as String)
|
|
||||||
.toList();
|
|
||||||
_giftFxLog(
|
_giftFxLog(
|
||||||
'recv LUCK_GIFT_ANIM_OTHER '
|
'recv LUCK_GIFT_ANIM_OTHER '
|
||||||
'giftPhoto=${msg.gift?.giftPhoto} '
|
'giftPhoto=${msg.gift?.giftPhoto} '
|
||||||
@ -6508,16 +6559,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
targetUserIds,
|
targetUserIds,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (msg.user != null && msg.toUser != null && msg.gift != null) {
|
if (!_shouldNotifyFloatingGiftListener(msg)) {
|
||||||
_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 {
|
|
||||||
_giftFxLog(
|
_giftFxLog(
|
||||||
'skip floating gift listener from LUCK_GIFT_ANIM_OTHER '
|
'skip floating gift listener from LUCK_GIFT_ANIM_OTHER '
|
||||||
'reason=incomplete_msg '
|
'reason=incomplete_msg '
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import 'dart:ui';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||||
|
|
||||||
|
void _seatFlightLog(String message) {
|
||||||
|
debugPrint('[GiftFx][seatFlight] $message');
|
||||||
|
}
|
||||||
|
|
||||||
typedef RoomGiftSeatTargetResolver =
|
typedef RoomGiftSeatTargetResolver =
|
||||||
GlobalKey<State<StatefulWidget>>? Function(String targetUserId);
|
GlobalKey<State<StatefulWidget>>? Function(String targetUserId);
|
||||||
|
|
||||||
@ -52,8 +56,19 @@ class RoomGiftSeatFlightController {
|
|||||||
if (_state == null) {
|
if (_state == null) {
|
||||||
_trimPendingRequestsOverflow();
|
_trimPendingRequestsOverflow();
|
||||||
_pendingRequests.add(normalizedRequest);
|
_pendingRequests.add(normalizedRequest);
|
||||||
|
_seatFlightLog(
|
||||||
|
'buffer request no state '
|
||||||
|
'target=${normalizedRequest.targetUserId} '
|
||||||
|
'queueTag=${normalizedRequest.queueTag} '
|
||||||
|
'pending=${_pendingRequests.length}',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_seatFlightLog(
|
||||||
|
'enqueue via attached state '
|
||||||
|
'target=${normalizedRequest.targetUserId} '
|
||||||
|
'queueTag=${normalizedRequest.queueTag}',
|
||||||
|
);
|
||||||
_state!._enqueue(normalizedRequest);
|
_state!._enqueue(normalizedRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,8 +91,20 @@ class RoomGiftSeatFlightController {
|
|||||||
_trimPendingRequestsForTag(queueTag, maxTrackedRequests);
|
_trimPendingRequestsForTag(queueTag, maxTrackedRequests);
|
||||||
_trimPendingRequestsOverflow();
|
_trimPendingRequestsOverflow();
|
||||||
_pendingRequests.add(normalizedRequest);
|
_pendingRequests.add(normalizedRequest);
|
||||||
|
_seatFlightLog(
|
||||||
|
'buffer limited request no state '
|
||||||
|
'target=${normalizedRequest.targetUserId} '
|
||||||
|
'queueTag=$queueTag '
|
||||||
|
'pending=${_pendingRequests.length}',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_seatFlightLog(
|
||||||
|
'enqueue limited via attached state '
|
||||||
|
'target=${normalizedRequest.targetUserId} '
|
||||||
|
'queueTag=$queueTag '
|
||||||
|
'max=$maxTrackedRequests',
|
||||||
|
);
|
||||||
_state!._enqueueLimited(
|
_state!._enqueueLimited(
|
||||||
normalizedRequest,
|
normalizedRequest,
|
||||||
maxTrackedRequests: maxTrackedRequests,
|
maxTrackedRequests: maxTrackedRequests,
|
||||||
@ -184,6 +211,7 @@ class RoomGiftSeatFlightController {
|
|||||||
|
|
||||||
void _attach(_RoomGiftSeatFlightOverlayState state) {
|
void _attach(_RoomGiftSeatFlightOverlayState state) {
|
||||||
_state = state;
|
_state = state;
|
||||||
|
_seatFlightLog('attach overlay pending=${_pendingRequests.length}');
|
||||||
while (_pendingRequests.isNotEmpty) {
|
while (_pendingRequests.isNotEmpty) {
|
||||||
state._enqueue(_pendingRequests.removeFirst());
|
state._enqueue(_pendingRequests.removeFirst());
|
||||||
}
|
}
|
||||||
@ -191,6 +219,7 @@ class RoomGiftSeatFlightController {
|
|||||||
|
|
||||||
void _detach(_RoomGiftSeatFlightOverlayState state) {
|
void _detach(_RoomGiftSeatFlightOverlayState state) {
|
||||||
if (_state == state) {
|
if (_state == state) {
|
||||||
|
_seatFlightLog('detach overlay');
|
||||||
_state = null;
|
_state = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,6 +291,13 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
_ensureCenterVisual(request);
|
_ensureCenterVisual(request);
|
||||||
_trimQueuedRequestsOverflow();
|
_trimQueuedRequestsOverflow();
|
||||||
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
|
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
|
||||||
|
_seatFlightLog(
|
||||||
|
'state enqueue '
|
||||||
|
'target=${request.targetUserId} '
|
||||||
|
'queue=${_queue.length} '
|
||||||
|
'queueTag=${request.queueTag} '
|
||||||
|
'batchTag=${request.batchTag}',
|
||||||
|
);
|
||||||
_syncIdleCenterVisual();
|
_syncIdleCenterVisual();
|
||||||
_scheduleNextAnimation();
|
_scheduleNextAnimation();
|
||||||
}
|
}
|
||||||
@ -411,6 +447,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
_queue.isEmpty ||
|
_queue.isEmpty ||
|
||||||
(expectedSessionToken != null &&
|
(expectedSessionToken != null &&
|
||||||
expectedSessionToken != _sessionToken)) {
|
expectedSessionToken != _sessionToken)) {
|
||||||
|
_seatFlightLog(
|
||||||
|
'skip start mounted=$mounted playing=$_isPlaying queue=${_queue.length} '
|
||||||
|
'expected=$expectedSessionToken session=$_sessionToken',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,9 +481,16 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final shouldRetryBatch = unresolvedBatch.any(
|
_seatFlightLog(
|
||||||
(queuedRequest) => queuedRequest.retryCount < 6,
|
'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) {
|
if (shouldRetryBatch) {
|
||||||
final retryBatch =
|
final retryBatch =
|
||||||
queuedBatch
|
queuedBatch
|
||||||
@ -466,6 +513,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (activeFlights.isEmpty) {
|
if (activeFlights.isEmpty) {
|
||||||
|
_seatFlightLog('drop batch no resolved targets');
|
||||||
_syncIdleCenterVisual();
|
_syncIdleCenterVisual();
|
||||||
_scheduleNextAnimation();
|
_scheduleNextAnimation();
|
||||||
return;
|
return;
|
||||||
@ -594,6 +642,13 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
final targetKey = widget.resolveTargetKey(targetUserId);
|
final targetKey = widget.resolveTargetKey(targetUserId);
|
||||||
final targetContext = targetKey?.currentContext;
|
final targetContext = targetKey?.currentContext;
|
||||||
if (overlayContext == null || targetContext == null) {
|
if (overlayContext == null || targetContext == null) {
|
||||||
|
_seatFlightLog(
|
||||||
|
'resolve target failed context '
|
||||||
|
'target=$targetUserId '
|
||||||
|
'hasOverlayContext=${overlayContext != null} '
|
||||||
|
'hasTargetKey=${targetKey != null} '
|
||||||
|
'hasTargetContext=${targetContext != null}',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -603,6 +658,12 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
targetBox is! RenderBox ||
|
targetBox is! RenderBox ||
|
||||||
!overlayBox.hasSize ||
|
!overlayBox.hasSize ||
|
||||||
!targetBox.hasSize) {
|
!targetBox.hasSize) {
|
||||||
|
_seatFlightLog(
|
||||||
|
'resolve target failed render '
|
||||||
|
'target=$targetUserId '
|
||||||
|
'overlayBox=${overlayBox.runtimeType} '
|
||||||
|
'targetBox=${targetBox.runtimeType}',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ class FloatingLuckyGiftGlobalScreenWidget extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.onAnimationCompleted,
|
required this.onAnimationCompleted,
|
||||||
|
this.debugDisableAutoSlide = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const int floatingType = 7;
|
static const int floatingType = 7;
|
||||||
@ -24,6 +25,7 @@ class FloatingLuckyGiftGlobalScreenWidget extends StatefulWidget {
|
|||||||
|
|
||||||
final SCFloatingMessage message;
|
final SCFloatingMessage message;
|
||||||
final VoidCallback onAnimationCompleted;
|
final VoidCallback onAnimationCompleted;
|
||||||
|
final bool debugDisableAutoSlide;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FloatingLuckyGiftGlobalScreenWidget> createState() =>
|
State<FloatingLuckyGiftGlobalScreenWidget> createState() =>
|
||||||
@ -73,7 +75,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
|||||||
});
|
});
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) {
|
if (mounted && !widget.debugDisableAutoSlide) {
|
||||||
_controller.forward();
|
_controller.forward();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -94,29 +96,19 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
|||||||
onTap: _handleTap,
|
onTap: _handleTap,
|
||||||
onHorizontalDragEnd: _handleHorizontalDragEnd,
|
onHorizontalDragEnd: _handleHorizontalDragEnd,
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation,
|
position:
|
||||||
|
widget.debugDisableAutoSlide
|
||||||
|
? const AlwaysStoppedAnimation(Offset.zero)
|
||||||
|
: (_isSwipeAnimating ? _swipeAnimation : _offsetAnimation),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||||
height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w,
|
height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
|
Positioned(left: 70.w, top: 34.w, child: _buildAvatar()),
|
||||||
Positioned.fill(child: _buildBackground()),
|
Positioned.fill(child: _buildBackground()),
|
||||||
Positioned(left: 30.w, top: 22.w, child: _buildAvatar()),
|
Positioned.fill(child: _buildContentOverlay()),
|
||||||
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(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -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() {
|
Widget _buildAvatar() {
|
||||||
const fallbackAsset = "sc_images/general/sc_icon_avar_defalt.png";
|
const fallbackAsset = "sc_images/general/sc_icon_avar_defalt.png";
|
||||||
final avatarUrl = (widget.message.userAvatarUrl ?? "").trim();
|
final avatarUrl = (widget.message.userAvatarUrl ?? "").trim();
|
||||||
@ -157,37 +171,28 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
|||||||
avatarUrl.isEmpty
|
avatarUrl.isEmpty
|
||||||
? Image.asset(
|
? Image.asset(
|
||||||
fallbackAsset,
|
fallbackAsset,
|
||||||
width: 48.w,
|
width: 26.w,
|
||||||
height: 48.w,
|
height: 26.w,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: Image(
|
: Image(
|
||||||
image: buildCachedImageProvider(
|
image: buildCachedImageProvider(
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
logicalWidth: 48.w,
|
logicalWidth: 38.w,
|
||||||
logicalHeight: 48.w,
|
logicalHeight: 38.w,
|
||||||
),
|
),
|
||||||
width: 48.w,
|
width: 38.w,
|
||||||
height: 48.w,
|
height: 38.w,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder:
|
errorBuilder:
|
||||||
(_, __, ___) => Image.asset(
|
(_, __, ___) => Image.asset(
|
||||||
fallbackAsset,
|
fallbackAsset,
|
||||||
width: 48.w,
|
width: 38.w,
|
||||||
height: 48.w,
|
height: 38.w,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Container(
|
return ClipOval(child: avatar);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextBlock() {
|
Widget _buildTextBlock() {
|
||||||
@ -201,20 +206,20 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
|||||||
title,
|
title,
|
||||||
TextStyle(
|
TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 19.sp,
|
fontSize: 14.sp,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w700,
|
||||||
height: 1,
|
height: 1,
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
shadows: _textShadows(),
|
shadows: _textShadows(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 7.w),
|
SizedBox(height: 1.w),
|
||||||
_singleLineText(
|
_singleLineText(
|
||||||
subtitle,
|
subtitle,
|
||||||
TextStyle(
|
TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 13.sp,
|
fontSize: 10.5.sp,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w600,
|
||||||
height: 1,
|
height: 1,
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
shadows: _textShadows(),
|
shadows: _textShadows(),
|
||||||
@ -234,7 +239,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
|||||||
textScaler: TextScaler.noScaling,
|
textScaler: TextScaler.noScaling,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 32.sp,
|
fontSize: 23.sp,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
height: 1,
|
height: 1,
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
|
|||||||
@ -377,41 +377,42 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
}
|
}
|
||||||
final imageReplacementsByName =
|
final imageReplacementsByName =
|
||||||
_imageReplacementsByName ?? const <String, Uint8List>{};
|
_imageReplacementsByName ?? const <String, Uint8List>{};
|
||||||
final useNativeAssetView =
|
final useNativeView = _shouldUseNativeRoomRocketPagView(
|
||||||
assetName != null && Platform.isIOS && widget.loop;
|
isIOS: Platform.isIOS,
|
||||||
|
hasAsset: assetName != null,
|
||||||
|
hasBytes: bytes != null,
|
||||||
|
);
|
||||||
final pagView =
|
final pagView =
|
||||||
assetName != null
|
assetName != null
|
||||||
? PAGView.asset(
|
? PAGView.asset(
|
||||||
assetName,
|
assetName,
|
||||||
key: _pagKey,
|
key: _pagKey,
|
||||||
width: useNativeAssetView ? widget.width : null,
|
width: useNativeView ? widget.width : null,
|
||||||
height: useNativeAssetView ? widget.height : null,
|
height: useNativeView ? widget.height : null,
|
||||||
autoPlay: useNativeAssetView,
|
autoPlay: useNativeView,
|
||||||
usePlatformView: useNativeAssetView,
|
usePlatformView: useNativeView,
|
||||||
repeatCount: repeatCount,
|
repeatCount: repeatCount,
|
||||||
onInit: useNativeAssetView ? null : _handleInit,
|
onInit: useNativeView ? null : _handleInit,
|
||||||
onAnimationStart:
|
onAnimationStart: useNativeView ? null : _handleAnimationStart,
|
||||||
useNativeAssetView ? null : _handleAnimationStart,
|
onAnimationEnd: useNativeView ? null : _handleAnimationEnd,
|
||||||
onAnimationEnd: useNativeAssetView ? null : _handleAnimationEnd,
|
onAnimationCancel: useNativeView ? null : _handleAnimationCancel,
|
||||||
onAnimationCancel:
|
onAnimationRepeat: useNativeView ? null : _handleAnimationRepeat,
|
||||||
useNativeAssetView ? null : _handleAnimationCancel,
|
|
||||||
onAnimationRepeat:
|
|
||||||
useNativeAssetView ? null : _handleAnimationRepeat,
|
|
||||||
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
||||||
imageReplacementsByName: imageReplacementsByName,
|
imageReplacementsByName: imageReplacementsByName,
|
||||||
)
|
)
|
||||||
: PAGView.bytes(
|
: PAGView.bytes(
|
||||||
bytes,
|
bytes,
|
||||||
key: _pagKey,
|
key: _pagKey,
|
||||||
width: null,
|
width: useNativeView ? widget.width : null,
|
||||||
height: null,
|
height: useNativeView ? widget.height : null,
|
||||||
autoPlay: false,
|
autoPlay: useNativeView,
|
||||||
|
usePlatformView: useNativeView,
|
||||||
repeatCount: repeatCount,
|
repeatCount: repeatCount,
|
||||||
onInit: _handleInit,
|
onInit: useNativeView ? null : _handleInit,
|
||||||
onAnimationStart: _handleAnimationStart,
|
onAnimationStart: useNativeView ? null : _handleAnimationStart,
|
||||||
onAnimationEnd: _handleAnimationEnd,
|
onAnimationEnd: useNativeView ? null : _handleAnimationEnd,
|
||||||
onAnimationCancel: _handleAnimationCancel,
|
onAnimationCancel: useNativeView ? null : _handleAnimationCancel,
|
||||||
onAnimationRepeat: _handleAnimationRepeat,
|
onAnimationRepeat: useNativeView ? null : _handleAnimationRepeat,
|
||||||
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
||||||
imageReplacementsByName: imageReplacementsByName,
|
imageReplacementsByName: imageReplacementsByName,
|
||||||
);
|
);
|
||||||
@ -422,7 +423,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
widget.clipBehavior == Clip.none ? Clip.none : Clip.hardEdge,
|
widget.clipBehavior == Clip.none ? Clip.none : Clip.hardEdge,
|
||||||
children: [
|
children: [
|
||||||
if (visibleUnderlay != null) visibleUnderlay,
|
if (visibleUnderlay != null) visibleUnderlay,
|
||||||
if (useNativeAssetView)
|
if (useNativeView)
|
||||||
Positioned.fill(child: pagView)
|
Positioned.fill(child: pagView)
|
||||||
else
|
else
|
||||||
_scaledPagView(pagView),
|
_scaledPagView(pagView),
|
||||||
@ -475,7 +476,11 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
urlsByName: imageReplacementUrls,
|
urlsByName: imageReplacementUrls,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (widget.loop) {
|
if (_shouldUseNativeRoomRocketPagView(
|
||||||
|
isIOS: Platform.isIOS,
|
||||||
|
hasAsset: true,
|
||||||
|
hasBytes: false,
|
||||||
|
)) {
|
||||||
_startWatchdog?.cancel();
|
_startWatchdog?.cancel();
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
@ -530,13 +535,25 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
|||||||
'head=${_pagByteHead(bytes)} '
|
'head=${_pagByteHead(bytes)} '
|
||||||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||||||
);
|
);
|
||||||
|
final useNativeView = _shouldUseNativeRoomRocketPagView(
|
||||||
|
isIOS: Platform.isIOS,
|
||||||
|
hasAsset: false,
|
||||||
|
hasBytes: true,
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_pagBytes = bytes;
|
_pagBytes = bytes;
|
||||||
_imageReplacementsByName = imageReplacements;
|
_imageReplacementsByName = imageReplacements;
|
||||||
_releasedFromMemory = false;
|
_releasedFromMemory = false;
|
||||||
|
if (useNativeView) {
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
_scheduleMemoryRelease(path);
|
_scheduleMemoryRelease(path);
|
||||||
_restartWatchdog();
|
if (useNativeView) {
|
||||||
|
_startWatchdog?.cancel();
|
||||||
|
} else {
|
||||||
|
_restartWatchdog();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!_isCurrentResource(path, signature)) {
|
if (!_isCurrentResource(path, signature)) {
|
||||||
return;
|
return;
|
||||||
@ -907,6 +924,27 @@ String debugRoomRocketPagSourceKind(String resource) {
|
|||||||
return _pagSourceKind(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<Uint8List> _readPagBytes(String value) async {
|
Future<Uint8List> _readPagBytes(String value) async {
|
||||||
final normalized = _normalizePagResource(value);
|
final normalized = _normalizePagResource(value);
|
||||||
final uri = _pagNetworkUri(normalized);
|
final uri = _pagNetworkUri(normalized);
|
||||||
|
|||||||
@ -1,14 +1,32 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:extended_image/extended_image.dart';
|
import 'package:extended_image/extended_image.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.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/shared/tools/sc_network_image_utils.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||||
|
|
||||||
const Duration _roomEmojiGifRestartInterval = Duration(milliseconds: 3500);
|
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 {
|
class RoomEmojiAssetImage extends StatefulWidget {
|
||||||
const RoomEmojiAssetImage({
|
const RoomEmojiAssetImage({
|
||||||
super.key,
|
super.key,
|
||||||
@ -71,7 +89,7 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
|||||||
height: widget.height,
|
height: widget.height,
|
||||||
loop: true,
|
loop: true,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
fallback: _buildNetworkFallback(resource, 'svga fallback'),
|
fallback: _buildNetworkFailure(resource, 'svga failed'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -135,6 +153,9 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
|||||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||||
cache: true,
|
cache: true,
|
||||||
clearMemoryCacheWhenDispose: false,
|
clearMemoryCacheWhenDispose: false,
|
||||||
|
clearMemoryCacheIfFailed: true,
|
||||||
|
cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width),
|
||||||
|
cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height),
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
@ -166,23 +187,19 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
|||||||
'failed error=$error',
|
'failed error=$error',
|
||||||
normalizedUrl,
|
normalizedUrl,
|
||||||
);
|
);
|
||||||
return _buildNetworkFallback(normalizedUrl, 'image failed');
|
return _buildNetworkFailure(normalizedUrl, 'image failed');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNetworkFallback(String resource, String reason) {
|
Widget _buildNetworkFailure(String resource, String reason) {
|
||||||
_logNetworkOnce(
|
_logNetworkOnce(
|
||||||
_loggedNetworkError,
|
_loggedNetworkError,
|
||||||
reason,
|
reason,
|
||||||
normalizeImageResourceUrl(resource),
|
normalizeImageResourceUrl(resource),
|
||||||
);
|
);
|
||||||
return SizedBox(
|
return SizedBox(width: widget.width, height: widget.height);
|
||||||
width: widget.width,
|
|
||||||
height: widget.height,
|
|
||||||
child: const Center(child: SCRotatingDotsLoading()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _logNetworkOnce(Set<String> bucket, String message, String url) {
|
void _logNetworkOnce(Set<String> bucket, String message, String url) {
|
||||||
@ -291,8 +308,11 @@ class _RestartingGifImageState extends State<_RestartingGifImage> {
|
|||||||
key: ValueKey('room_emoji_gif_placeholder_${normalizedUrl}_$token'),
|
key: ValueKey('room_emoji_gif_placeholder_${normalizedUrl}_$token'),
|
||||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||||
cache: true,
|
cache: true,
|
||||||
cacheKey: '$normalizedUrl#room_emoji_gif_${token % 2}',
|
cacheKey: _roomEmojiCacheKey(normalizedUrl, token % 2),
|
||||||
clearMemoryCacheWhenDispose: false,
|
clearMemoryCacheWhenDispose: false,
|
||||||
|
clearMemoryCacheIfFailed: true,
|
||||||
|
cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width),
|
||||||
|
cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height),
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
@ -317,8 +337,11 @@ class _RestartingGifImageState extends State<_RestartingGifImage> {
|
|||||||
),
|
),
|
||||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||||
cache: true,
|
cache: true,
|
||||||
cacheKey: '$normalizedUrl#room_emoji_gif_${_restartToken % 2}',
|
cacheKey: _roomEmojiCacheKey(normalizedUrl, _restartToken % 2),
|
||||||
clearMemoryCacheWhenDispose: true,
|
clearMemoryCacheWhenDispose: true,
|
||||||
|
clearMemoryCacheIfFailed: true,
|
||||||
|
cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width),
|
||||||
|
cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height),
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
@ -365,11 +388,7 @@ class _RestartingGifImageState extends State<_RestartingGifImage> {
|
|||||||
case LoadState.failed:
|
case LoadState.failed:
|
||||||
final error = state.lastException ?? 'unknown';
|
final error = state.lastException ?? 'unknown';
|
||||||
_debugGifOnce(_loggedError, 'failed error=$error', normalizedUrl);
|
_debugGifOnce(_loggedError, 'failed error=$error', normalizedUrl);
|
||||||
return SizedBox(
|
return SizedBox(width: widget.width, height: widget.height);
|
||||||
width: widget.width,
|
|
||||||
height: widget.height,
|
|
||||||
child: const Center(child: SCRotatingDotsLoading()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1781,7 +1781,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.4.2"
|
version: "4.4.2"
|
||||||
webview_flutter_android:
|
webview_flutter_android:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_android
|
name: webview_flutter_android
|
||||||
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"
|
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"
|
||||||
|
|||||||
@ -58,6 +58,7 @@ dependencies:
|
|||||||
#权限请求
|
#权限请求
|
||||||
permission_handler: ^12.0.0+1
|
permission_handler: ^12.0.0+1
|
||||||
webview_flutter: 4.4.2
|
webview_flutter: 4.4.2
|
||||||
|
webview_flutter_android: 3.16.9
|
||||||
#跳转外链
|
#跳转外链
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
#键值对存储
|
#键值对存储
|
||||||
|
|||||||
32
test/room_game_loading_view_test.dart
Normal file
32
test/room_game_loading_view_test.dart
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -44,6 +44,33 @@ void main() {
|
|||||||
expect(debugRoomRocketPagSourceKind('assets/rocket/rocket.pag'), 'asset');
|
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', () {
|
test('keeps pag bytes in memory for five minutes by default', () {
|
||||||
expect(debugRoomRocketPagMemoryTtl(), const Duration(minutes: 5));
|
expect(debugRoomRocketPagMemoryTtl(), const Duration(minutes: 5));
|
||||||
});
|
});
|
||||||
|
|||||||
1
需求进度.md
1
需求进度.md
@ -1055,3 +1055,4 @@
|
|||||||
- [x] 2026-06-02 继续优化公屏 GIF 循环闪 loading:网络 GIF 重播时不再销毁整个 widget,加载下一轮期间复用上一帧画面,避免循环间隙出现转圈。
|
- [x] 2026-06-02 继续优化公屏 GIF 循环闪 loading:网络 GIF 重播时不再销毁整个 widget,加载下一轮期间复用上一帧画面,避免循环间隙出现转圈。
|
||||||
- [x] 2026-06-02 复核发现不销毁 key 会导致 GIF 解码器不重启并停在最后一帧;改为前景层用新 key 重播、背景层保留上一轮画面,兼顾循环播放和不显示 loading。
|
- [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 按接口文档恢复 `have` 权限判断:`/material/emoji/all` 返回的表情包仅 `have == true` 且非空时展示,`have=false/null` 计入 `filteredNoAccess` 日志。
|
||||||
|
- [x] 2026-06-02 排查 Android 表情包一直转圈:初步判断不是接口解析,表情组已进入渲染后媒体加载失败态被转圈 fallback 掩盖;表情图片/GIF 按项目通用 `netImage` 补解码尺寸、失败清缓存,并把失败 fallback 改为静态占位便于区分 loading 与 failed。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user