import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/widgets/room/music/room_music_floating_entry.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_pending_cache.dart'; import 'package:yumi/ui_kit/widgets/room/room_banner_view.dart'; import 'package:yumi/ui_kit/widgets/room/room_game_bottom_sheet.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart'; class RoomPlayWidget extends StatefulWidget { const RoomPlayWidget({super.key}); @override State createState() => _RoomPlayWidgetState(); } class _RoomPlayWidgetState extends State { static const double _rightActionDownShift = 24; @override Widget build(BuildContext context) { return SizedBox.expand( child: Stack( children: [ PositionedDirectional( end: 15.w, bottom: (150 - _rightActionDownShift).w, child: const RoomMusicFloatingEntry(), ), PositionedDirectional( end: 65.w, bottom: (150 - _rightActionDownShift).w, child: const RoomRocketFloatingEntry(), ), PositionedDirectional( end: 15.w, bottom: (200 - _rightActionDownShift).w, child: const _RoomRedPacketFloatingEntry(), ), PositionedDirectional( end: 15.w, bottom: (100 - _rightActionDownShift).w, child: const RoomGameEntryButton(), ), PositionedDirectional( end: 10.w, bottom: 15.w, child: SizedBox(width: 46.w, child: RoomBannerView()), ), ], ), ); } } class _RoomRedPacketFloatingEntry extends StatefulWidget { const _RoomRedPacketFloatingEntry(); @override State<_RoomRedPacketFloatingEntry> createState() => _RoomRedPacketFloatingEntryState(); } class _RoomRedPacketFloatingEntryState extends State<_RoomRedPacketFloatingEntry> { static const int _listRefreshIntervalSeconds = 5; Timer? _entryRefreshTimer; bool _refreshingList = false; int _elapsedSeconds = 0; String _observedRoomId = ''; String _verifiedRoomId = ''; int _verifiedRoomRefreshCount = 0; Set _verifiedKnownPacketIds = const {}; Set _verifiedVisiblePacketIds = const {}; @override void initState() { super.initState(); _entryRefreshTimer = Timer.periodic( const Duration(seconds: 1), (_) => _onEntryTimerTick(), ); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _refreshRoomRedPacketList(); } }); } @override void dispose() { _entryRefreshTimer?.cancel(); super.dispose(); } void _onEntryTimerTick() { RoomRedPacketPendingCache.instance.pruneExpired(); if (!mounted) { return; } _refreshRoomRedPacketListIfRoomChanged(); setState(() {}); _elapsedSeconds++; if (_elapsedSeconds % _listRefreshIntervalSeconds == 0) { _refreshRoomRedPacketList(); } } void _refreshRoomRedPacketListIfRoomChanged() { try { final rtcProvider = Provider.of(context, listen: false); final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; if (currentRoomId.isEmpty || currentRoomId == _observedRoomId) { return; } _observedRoomId = currentRoomId; _elapsedSeconds = 0; _verifiedRoomId = ''; _verifiedRoomRefreshCount = 0; _verifiedKnownPacketIds = const {}; _verifiedVisiblePacketIds = const {}; _refreshRoomRedPacketList(); } catch (_) {} } void _refreshRoomRedPacketList() { if (_refreshingList || !mounted) { return; } try { final rtcProvider = Provider.of(context, listen: false); final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; if (currentRoomId.isEmpty) { return; } _refreshingList = true; unawaited( rtcProvider .loadRoomRedPacketList(1) .then((result) { final activeRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?.trim() ?? ''; if (activeRoomId != currentRoomId) { return result; } final serverPackets = result.map(RoomRedPacketUiData.fromListRes).toList(); final serverPacketIds = serverPackets .map((item) => item.packetId.trim()) .where((packetId) => packetId.isNotEmpty) .toSet(); final visiblePacketIds = serverPackets .map(RoomRedPacketOpenDialog.withLocalClaimState) .where(_shouldShowRoomRedPacketEntry) .map((item) => item.packetId.trim()) .where((packetId) => packetId.isNotEmpty) .toSet(); if (!mounted) { return result; } setState(() { if (_verifiedRoomId == currentRoomId) { _verifiedRoomRefreshCount++; _verifiedKnownPacketIds = { ..._verifiedKnownPacketIds, ...serverPacketIds, }; } else { _verifiedRoomId = currentRoomId; _verifiedRoomRefreshCount = 1; _verifiedKnownPacketIds = serverPacketIds; } _verifiedVisiblePacketIds = visiblePacketIds; }); return result; }) .catchError((_) => rtcProvider.redPacketList) .whenComplete(() { _refreshingList = false; }), ); } catch (_) { _refreshingList = false; } } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: Listenable.merge([ RoomRedPacketPendingCache.instance, RoomRedPacketOpenDialog.claimedStateNotifier, ]), builder: (context, child) { return Consumer2( builder: (context, rtcProvider, rtmProvider, child) { final snapshot = _RoomRedPacketEntrySnapshot.fromProviders( rtcProvider: rtcProvider, rtmProvider: rtmProvider, verifiedRoomId: _verifiedRoomId, verifiedRoomRefreshCount: _verifiedRoomRefreshCount, verifiedKnownPacketIds: _verifiedKnownPacketIds, verifiedVisiblePacketIds: _verifiedVisiblePacketIds, ); if (snapshot.packets.isEmpty) { return const SizedBox.shrink(); } return SCDebounceWidget( onTap: () { final freshSnapshot = _RoomRedPacketEntrySnapshot.fromProviders( rtcProvider: Provider.of(context, listen: false), rtmProvider: Provider.of(context, listen: false), verifiedRoomId: _verifiedRoomId, verifiedRoomRefreshCount: _verifiedRoomRefreshCount, verifiedKnownPacketIds: _verifiedKnownPacketIds, verifiedVisiblePacketIds: _verifiedVisiblePacketIds, ); if (freshSnapshot.packets.isEmpty) { return; } RoomRedPacketOpenDialog.showList( context, freshSnapshot.packets, ); }, child: SizedBox( width: 46.w, height: 46.w, child: Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ Image.asset( 'sc_images/room/red_packet/small_packet_icon.png', width: 44.w, height: 44.w, fit: BoxFit.contain, gaplessPlayback: true, ), if (snapshot.packets.length > 1) PositionedDirectional( top: -2.w, end: -2.w, child: Container( constraints: BoxConstraints(minWidth: 16.w), height: 16.w, padding: EdgeInsets.symmetric(horizontal: 4.w), alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFFFF3D34), borderRadius: BorderRadius.circular(8.w), border: Border.all(color: Colors.white, width: 1.w), ), child: Text( snapshot.packets.length > 99 ? '99+' : snapshot.packets.length.toString(), textScaler: TextScaler.noScaling, maxLines: 1, style: TextStyle( color: Colors.white, fontSize: 9.sp, fontWeight: FontWeight.w800, decoration: TextDecoration.none, ), ), ), ), ], ), ), ); }, ); }, ); } } class _RoomRedPacketEntrySnapshot { const _RoomRedPacketEntrySnapshot(this.packets, this.signature); factory _RoomRedPacketEntrySnapshot.fromProviders({ required RtcProvider rtcProvider, required RtmProvider rtmProvider, required String verifiedRoomId, required int verifiedRoomRefreshCount, required Set verifiedKnownPacketIds, required Set verifiedVisiblePacketIds, }) { final packets = []; final hiddenPacketIds = {}; final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; final shouldTrustVerifiedAbsence = currentRoomId.isNotEmpty && verifiedRoomId == currentRoomId && verifiedRoomRefreshCount >= 2; for (final packet in rtcProvider.redPacketList.map( RoomRedPacketUiData.fromListRes, )) { _addVisiblePacket( packets, packet, hiddenPacketIds: hiddenPacketIds, authoritative: true, ); } for (final packet in RoomRedPacketPendingCache.instance.packetsForRoom( currentRoomId, )) { _addVisiblePacket( packets, packet, hiddenPacketIds: hiddenPacketIds, verifiedKnownPacketIds: shouldTrustVerifiedAbsence ? verifiedKnownPacketIds : null, verifiedVisiblePacketIds: shouldTrustVerifiedAbsence ? verifiedVisiblePacketIds : null, ); } for (final msg in rtmProvider.roomChatMsgList) { if (msg.type != SCRoomMsgType.roomRedPacket) { continue; } _addVisiblePacket( packets, RoomRedPacketUiData.fromMessagePayload( payload: msg.msg, fallbackUser: msg.user, ), hiddenPacketIds: hiddenPacketIds, verifiedKnownPacketIds: shouldTrustVerifiedAbsence ? verifiedKnownPacketIds : null, verifiedVisiblePacketIds: shouldTrustVerifiedAbsence ? verifiedVisiblePacketIds : null, ); } final signature = packets .map( (item) => '${item.packetId}|${item.status}|${item.remainCount}|${item.grabbed}|${item.claimedAmount}|${item.claimStartTimeMs}|${item.expireTimeMs}', ) .join(';'); return _RoomRedPacketEntrySnapshot(packets, signature); } final List packets; final String signature; @override bool operator ==(Object other) { if (identical(this, other)) { return true; } return other is _RoomRedPacketEntrySnapshot && other.signature == signature; } @override int get hashCode => signature.hashCode; } void _addVisiblePacket( List packets, RoomRedPacketUiData packet, { Set? hiddenPacketIds, Set? verifiedKnownPacketIds, Set? verifiedVisiblePacketIds, bool authoritative = false, }) { final normalizedPacket = RoomRedPacketOpenDialog.withLocalClaimState(packet); final packetId = normalizedPacket.packetId.trim(); if (packetId.isNotEmpty && RoomRedPacketOpenDialog.isClaimedLocally(packetId)) { if (authoritative) { hiddenPacketIds?.add(packetId); } return; } if (packetId.isNotEmpty && RoomRedPacketOpenDialog.isUnavailableLocally(packetId)) { if (authoritative) { hiddenPacketIds?.add(packetId); } return; } if (!authoritative && packetId.isNotEmpty && (hiddenPacketIds?.contains(packetId) ?? false)) { return; } if (!authoritative && packetId.isNotEmpty && verifiedKnownPacketIds != null && verifiedVisiblePacketIds != null && verifiedKnownPacketIds.contains(packetId) && !verifiedVisiblePacketIds.contains(packetId)) { return; } if (!_shouldShowRoomRedPacketEntry(normalizedPacket)) { if (authoritative && packetId.isNotEmpty) { hiddenPacketIds?.add(packetId); } return; } if (packetId.isNotEmpty && packets.any((item) => item.packetId.trim() == packetId)) { final existingIndex = packets.indexWhere( (item) => item.packetId.trim() == packetId, ); packets[existingIndex] = packets[existingIndex].withFallbackProfile( normalizedPacket, ); return; } packets.add(normalizedPacket); } bool _shouldShowRoomRedPacketEntry(RoomRedPacketUiData item) { return !item.grabbed && !item.isExpired && !item.isSoldOut; }