import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/data_sources/models/enum/sc_room_special_mike_type.dart'; ///麦位 class SCSeatItem extends StatefulWidget { final num index; final bool isGameModel; const SCSeatItem({Key? key, required this.index, this.isGameModel = false}) : super(key: key); @override _SCSeatItemState createState() => _SCSeatItemState(); } class _SCSeatItemState extends State with TickerProviderStateMixin { static const String _seatHeartbeatValueIconAsset = "sc_images/room/sc_icon_room_seat_heartbeat_value.png"; static const double _seatHeaddressScaleMultiplier = 1.3; RtcProvider? provider; _SeatRenderSnapshot? _cachedSnapshot; final GlobalKey _targetKey = GlobalKey(); @override void initState() { super.initState(); provider = Provider.of(context, listen: false); provider?.bindTargetKey(widget.index, _targetKey); } @override Widget build(BuildContext context) { return Selector( selector: (context, provider) => _SeatRenderSnapshot.fromProvider(provider, widget.index), builder: (context, liveSnapshot, child) { final seatSnapshot = _resolveSeatSnapshot(liveSnapshot); final resolvedHeaddress = SCPathUtils.getFileExtension( seatSnapshot.headdressSourceUrl, ).toLowerCase() == ".mp4" && window.locale.languageCode == "ar" ? "" : seatSnapshot.headdressSourceUrl; final double seatBaseSize = widget.isGameModel ? 40.w : 55.w; final bool hasHeaddress = resolvedHeaddress.isNotEmpty; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { context.read().clickSite(widget.index); }, child: Column( children: [ SizedBox( key: _targetKey, width: seatBaseSize, height: seatBaseSize, child: Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ Sonic( index: widget.index, specialMikeType: seatSnapshot.specialMikeType, isGameModel: widget.isGameModel, ), seatSnapshot.hasUser ? Transform.scale( scale: hasHeaddress ? _seatHeaddressScaleMultiplier : 1, child: head( url: seatSnapshot.userAvatar, width: seatBaseSize, height: seatBaseSize, headdress: resolvedHeaddress, ), ) : (seatSnapshot.micLock ? Image.asset( "sc_images/room/sc_icon_seat_lock.png", width: widget.isGameModel ? 38.w : 52.w, height: widget.isGameModel ? 38.w : 52.w, ) : Image.asset( "sc_images/room/sc_icon_seat_open.png", width: widget.isGameModel ? 38.w : 52.w, height: widget.isGameModel ? 38.w : 52.w, )), Positioned( bottom: widget.isGameModel ? 2.w : 5.w, right: widget.isGameModel ? 2.w : 5.w, child: seatSnapshot.micMute ? Image.asset( "sc_images/room/sc_icon_room_seat_mic_mute.png", width: 14.w, height: 14.w, ) : Container(), ), IgnorePointer( child: Emoticons( index: widget.index, isGameModel: widget.isGameModel, ), ), ], ), ), widget.isGameModel ? Container() : (seatSnapshot.hasUser ? SizedBox( width: 64.w, child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ msgRoleTag( seatSnapshot.userRoles, width: 15.w, height: 15.w, ), Flexible( child: socialchatNickNameText( fontWeight: FontWeight.w600, seatSnapshot.userNickname, fontSize: 10.sp, type: seatSnapshot.userVipName, needScroll: seatSnapshot .userNickname .characters .length > 8, ), ), ], ), SizedBox(height: 2.w), SizedBox( height: 10.w, child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Image.asset( _seatHeartbeatValueIconAsset, width: 8.w, height: 8.w, fit: BoxFit.contain, ), SizedBox(width: 2.w), text( _heartbeatVaFormat( seatSnapshot.userHeartbeatValue, ), fontWeight: FontWeight.w600, fontSize: 8.sp, lineHeight: 1, ), ], ), ), ], ), ) : Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(height: 16.w), text( "NO.${widget.index}", fontSize: 10.sp, fontWeight: FontWeight.w600, ), ], )), ], ), ); }, ); } _SeatRenderSnapshot _resolveSeatSnapshot(_SeatRenderSnapshot liveSnapshot) { if (liveSnapshot.isExitingCurrentVoiceRoomSession) { return _cachedSnapshot ?? liveSnapshot; } _cachedSnapshot = liveSnapshot; return liveSnapshot; } String _heartbeatVaFormat(num value) { final resolvedValue = value.toInt(); if (resolvedValue >= 1000000) { return "${(resolvedValue / 1000000).toStringAsFixed(1)}M"; } if (resolvedValue >= 10000) { return "${(resolvedValue / 1000).toStringAsFixed(0)}k"; } return "$resolvedValue"; } } class _SeatRenderSnapshot { const _SeatRenderSnapshot({ required this.isExitingCurrentVoiceRoomSession, required this.specialMikeType, required this.userId, required this.userAvatar, required this.headdressSourceUrl, required this.userNickname, required this.userRoles, required this.userVipName, required this.userHeartbeatValue, required this.micLock, required this.micMute, }); factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) { final roomSeat = provider.micAtIndexForDisplay(index); final user = roomSeat?.user; return _SeatRenderSnapshot( isExitingCurrentVoiceRoomSession: provider.isExitingCurrentVoiceRoomSession, specialMikeType: provider.currenRoom?.roomProfile?.roomSetting?.roomSpecialMikeType ?? "", userId: user?.id ?? "", userAvatar: user?.userAvatar ?? "", headdressSourceUrl: user?.getHeaddress()?.sourceUrl ?? "", userNickname: user?.userNickname ?? "", userRoles: user?.roles ?? "", userVipName: user?.getVIP()?.name ?? "", userHeartbeatValue: user?.heartbeatVal ?? 0, micLock: roomSeat?.micLock ?? false, micMute: roomSeat?.micMute ?? false, ); } final bool isExitingCurrentVoiceRoomSession; final String specialMikeType; final String userId; final String userAvatar; final String headdressSourceUrl; final String userNickname; final String userRoles; final String userVipName; final num userHeartbeatValue; final bool micLock; final bool micMute; bool get hasUser => userId.isNotEmpty; @override bool operator ==(Object other) { if (identical(this, other)) { return true; } return other is _SeatRenderSnapshot && other.isExitingCurrentVoiceRoomSession == isExitingCurrentVoiceRoomSession && other.specialMikeType == specialMikeType && other.userId == userId && other.userAvatar == userAvatar && other.headdressSourceUrl == headdressSourceUrl && other.userNickname == userNickname && other.userRoles == userRoles && other.userVipName == userVipName && other.userHeartbeatValue == userHeartbeatValue && other.micLock == micLock && other.micMute == micMute; } @override int get hashCode => Object.hash( isExitingCurrentVoiceRoomSession, specialMikeType, userId, userAvatar, headdressSourceUrl, userNickname, userRoles, userVipName, userHeartbeatValue, micLock, micMute, ); } class _SeatEmojiSnapshot { const _SeatEmojiSnapshot({ required this.userId, required this.emojiPath, required this.type, required this.number, }); factory _SeatEmojiSnapshot.fromMic(MicRes? mic) { final emojiPath = mic?.emojiPath ?? ""; return _SeatEmojiSnapshot( userId: mic?.user?.id ?? "", emojiPath: emojiPath.isEmpty ? null : emojiPath, type: mic?.type ?? "", number: mic?.number ?? "", ); } final String userId; final String? emojiPath; final String type; final String number; bool get hasUser => userId.isNotEmpty; @override bool operator ==(Object other) { if (identical(this, other)) { return true; } return other is _SeatEmojiSnapshot && other.userId == userId && other.emojiPath == emojiPath && other.type == type && other.number == number; } @override int get hashCode => Object.hash(userId, emojiPath, type, number); } class _SeatEmojiEvent { const _SeatEmojiEvent({required this.type, required this.number}); final String type; final String number; @override String toString() => "type=$type number=$number"; } class Emoticons extends StatefulWidget { final num index; final bool isGameModel; const Emoticons({Key? key, required this.index, this.isGameModel = false}) : super(key: key); @override _EmoticonsState createState() => _EmoticonsState(); } class _EmoticonsState extends State with TickerProviderStateMixin { late AnimationController emoticonsAniCtr; late CurvedAnimation curvedAnimation; bool showIn = false; _SeatEmojiEvent? playingEvent; List<_SeatEmojiEvent> pathList = []; String? giftPath; List giftList = []; bool showResult = false; @override void initState() { // TODO: implement initState emoticonsAniCtr = AnimationController( vsync: this, duration: Duration(milliseconds: 350), ); curvedAnimation = CurvedAnimation( curve: Curves.ease, parent: emoticonsAniCtr, ); emoticonsAniCtr.addStatusListener((status) { if (status == AnimationStatus.dismissed) { _checkStart(); } }); super.initState(); } @override Widget build(BuildContext context) { return Selector( selector: (context, provider) => _SeatEmojiSnapshot.fromMic( provider.micAtIndexForDisplay(widget.index), ), builder: ( BuildContext context, _SeatEmojiSnapshot snapshot, Widget? child, ) { if (!snapshot.hasUser) { return Container(); } if (snapshot.emojiPath != null) { pathList.add( _SeatEmojiEvent(type: snapshot.type, number: snapshot.number), ); context.read().roomWheatMap[widget.index]?.setEmojiPath = null; _checkStart(); } if (playingEvent != null) { return FadeTransition( opacity: curvedAnimation, child: playingEvent?.type == SCRoomMsgType.roomDice ? FutureBuilder( future: Future.delayed(Duration(milliseconds: 2000)), // 传入Future builder: ( BuildContext context, AsyncSnapshot snapshot, ) { // 根据snapshot的状态来构建UI if (snapshot.connectionState == ConnectionState.done) { return Image.asset( "sc_images/room/sc_icon_dice_${playingEvent?.number}.png", height: widget.isGameModel ? 38.w : 45.w, ); } else { return Image.asset( "sc_images/room/sc_icon_dice_animl.webp", height: widget.isGameModel ? 38.w : 45.w, ); } }, ) : (playingEvent?.type == SCRoomMsgType.roomRPS ? FutureBuilder( future: Future.delayed(Duration(milliseconds: 2000)), // 传入Future builder: ( BuildContext context, AsyncSnapshot snapshot, ) { // 根据snapshot的状态来构建UI if (snapshot.connectionState == ConnectionState.done) { return Image.asset( "sc_images/room/sc_icon_rps_${playingEvent?.number}.png", height: widget.isGameModel ? 38.w : 45.w, ); } else { return Image.asset( "sc_images/room/sc_icon_rps_animal.webp", height: widget.isGameModel ? 38.w : 45.w, ); } }, ) : netImage( url: playingEvent?.number ?? "", width: widget.isGameModel ? 65.w : 75.w, )), ); } else if (giftPath != null) { return FadeTransition( opacity: curvedAnimation, child: netImage( url: "$giftPath", width: widget.isGameModel ? 65.w : 75.w, borderRadius: BorderRadius.circular(4.w), ), ); } return Container(); }, ); } void _checkStart() { if (!showIn) { if (pathList.isNotEmpty) { playingEvent = pathList.first; pathList.removeAt(0); emoticonsAniCtr.forward(); Future.delayed(Duration(milliseconds: 5)).then((value) { setState(() { showIn = true; }); }); /// 延迟3秒移除表情包 Future.delayed(Duration(seconds: 3)).then((value) { emoticonsAniCtr.reverse(); showIn = false; playingEvent = null; _checkStart(); }); } else if (giftList.isNotEmpty) { giftPath = giftList.first; giftList.removeAt(0); emoticonsAniCtr.forward(); Future.delayed(Duration(milliseconds: 10)).then((value) { setState(() { showIn = true; }); }); /// 延迟3秒移除表情包 Future.delayed(Duration(seconds: 1)).then((value) { emoticonsAniCtr.reverse(); showIn = false; giftPath = null; _checkStart(); }); } } } } class Sonic extends StatefulWidget { final num index; final bool isGameModel; final String specialMikeType; const Sonic({ Key? key, required this.index, required this.specialMikeType, this.isGameModel = false, }) : super(key: key); @override _SonicState createState() => _SonicState(); } class _SonicState extends State with TickerProviderStateMixin { List ctrList = []; int lastIndex = 0; late RtcProvider provider; @override void initState() { // TODO: implement initState super.initState(); for (int i = 0; i < 4; i++) { ctrList.add( AnimationController(duration: Duration(milliseconds: 900), vsync: this), ); } for (var element in ctrList) { element.addStatusListener((status) { if (status == AnimationStatus.completed) { element.reset(); } }); } provider = Provider.of(context, listen: false); provider.addSoundVoiceChangeListener(_onVoiceChange); } @override void dispose() { // TODO: implement dispose provider.removeSoundVoiceChangeListener(_onVoiceChange); for (var element in ctrList) { element.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return Stack( clipBehavior: Clip.none, children: [ SonicItem( ctrList[0], widget.specialMikeType, isGameModel: widget.isGameModel, ), SonicItem( ctrList[1], widget.specialMikeType, isGameModel: widget.isGameModel, ), SonicItem( ctrList[2], widget.specialMikeType, isGameModel: widget.isGameModel, ), SonicItem( ctrList[3], widget.specialMikeType, isGameModel: widget.isGameModel, ), ], ); } void _checkSoundAni(int volume) async { if (volume <= 20) return; for (final element in ctrList) { if (element.value == 0) { if (lastIndex == 0) { element.forward(); lastIndex = ctrList.indexOf(element); return; } else { if (ctrList[lastIndex].value >= 0.35 || ctrList[lastIndex].status == AnimationStatus.dismissed) { element.forward(); lastIndex = ctrList.indexOf(element); return; } } } } } _onVoiceChange(num index, int volum) { if (widget.index == index) { _checkSoundAni(volum); } } } class SonicItem extends AnimatedWidget { final Animation animation; final String specialMikeType; final bool isGameModel; SonicItem( this.animation, this.specialMikeType, { super.key, this.isGameModel = false, }) : super(listenable: animation); @override Widget build(BuildContext context) { return Visibility( visible: animation.value > 0, child: ScaleTransition( scale: Tween(begin: 1.0, end: 1.4).animate(animation), child: Container( height: width(isGameModel ? 34 : 52), width: width(isGameModel ? 34 : 52), decoration: specialMikeType.isEmpty || (specialMikeType != SCRoomSpecialMikeType.TYPE_VIP3.name && specialMikeType != SCRoomSpecialMikeType.TYPE_VIP4.name && specialMikeType != SCRoomSpecialMikeType.TYPE_VIP5.name && specialMikeType != SCRoomSpecialMikeType.TYPE_VIP6.name) ? BoxDecoration( shape: BoxShape.circle, color: SocialChatTheme.primaryColor.withValues( alpha: 1 - animation.value, ), ) : BoxDecoration(), child: specialMikeType == SCRoomSpecialMikeType.TYPE_VIP3.name ? Image.asset( "sc_images/room/sc_icon_room_vip3_sonic_anim.webp", ) : (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP4.name ? Image.asset( "sc_images/room/sc_icon_room_vip4_sonic_anim.webp", ) : (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP5.name ? Image.asset( "sc_images/room/sc_icon_room_vip5_sonic_anim.webp", ) : (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP6.name ? Image.asset( "sc_images/room/sc_icon_room_vip6_sonic_anim.webp", ) : Container()))), ), ), ); } }