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'; class RoomPlayWidget extends StatefulWidget { const RoomPlayWidget({super.key}); @override State createState() => _RoomPlayWidgetState(); } class _RoomPlayWidgetState extends State { @override Widget build(BuildContext context) { return SizedBox.expand( child: Stack( children: [ PositionedDirectional( end: 15.w, bottom: 150.w, child: const RoomMusicFloatingEntry(), ), PositionedDirectional( end: 15.w, bottom: 200.w, child: const _RoomRedPacketFloatingEntry(), ), PositionedDirectional( end: 15.w, bottom: 100.w, child: const RoomGameEntryButton(), ), PositionedDirectional( end: 10.w, bottom: 15.w, child: SizedBox(width: 46.w, child: RoomBannerView()), ), ], ), ); } } class _RoomRedPacketFloatingEntry extends StatelessWidget { const _RoomRedPacketFloatingEntry(); @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, ); 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), ); if (freshSnapshot.packets.isEmpty) { return; } RoomRedPacketOpenDialog.showList( context, freshSnapshot.packets, ); }, child: SizedBox( width: 44.w, height: 44.w, child: Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ Container( width: 42.w, height: 42.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.black.withValues(alpha: 0.22), border: Border.all( color: const Color(0xFFFFD56A), width: 1.3.w, ), ), alignment: Alignment.center, child: Image.asset( 'sc_images/room/red_packet/small_packet_icon.png', width: 32.w, height: 32.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, }) { final packets = []; final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; for (final packet in rtcProvider.redPacketList.map( RoomRedPacketUiData.fromListRes, )) { _addVisiblePacket(packets, packet); } for (final packet in RoomRedPacketPendingCache.instance.packetsForRoom( currentRoomId, )) { _addVisiblePacket(packets, packet); } for (final msg in rtmProvider.roomChatMsgList) { if (msg.type != SCRoomMsgType.roomRedPacket) { continue; } _addVisiblePacket( packets, RoomRedPacketUiData.fromMessagePayload( payload: msg.msg, fallbackUser: msg.user, ), ); } 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, ) { final normalizedPacket = RoomRedPacketOpenDialog.withLocalClaimState(packet); if (!_shouldShowRoomRedPacketEntry(normalizedPacket)) { return; } final packetId = normalizedPacket.packetId.trim(); if (packetId.isNotEmpty && packets.any((item) => item.packetId.trim() == packetId)) { return; } packets.add(normalizedPacket); } bool _shouldShowRoomRedPacketEntry(RoomRedPacketUiData item) { final normalizedStatus = item.statusDesc.toLowerCase(); final explicitSoldOut = item.status == 2 || normalizedStatus.contains('finish') || normalizedStatus.contains('sold'); return !item.grabbed && !item.isExpired && !explicitSoldOut; }