chatapp3-flutter/lib/modules/room/seat/sc_seat_item.dart
NIGGER SLAYER c4e177dd7e bug fix
2026-04-21 18:25:03 +08:00

709 lines
24 KiB
Dart

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<SCSeatItem> 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<RtcProvider>(context, listen: false);
provider?.bindTargetKey(widget.index, _targetKey);
}
@override
Widget build(BuildContext context) {
return Selector<RtcProvider, _SeatRenderSnapshot>(
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<RtcProvider>().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<Emoticons> with TickerProviderStateMixin {
late AnimationController emoticonsAniCtr;
late CurvedAnimation curvedAnimation;
bool showIn = false;
_SeatEmojiEvent? playingEvent;
List<_SeatEmojiEvent> pathList = [];
String? giftPath;
List<String> 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<RtcProvider, _SeatEmojiSnapshot>(
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<RtcProvider>().roomWheatMap[widget.index]?.setEmojiPath =
null;
_checkStart();
}
if (playingEvent != null) {
return FadeTransition(
opacity: curvedAnimation,
child:
playingEvent?.type == SCRoomMsgType.roomDice
? FutureBuilder<void>(
future: Future.delayed(Duration(milliseconds: 2000)),
// 传入Future
builder: (
BuildContext context,
AsyncSnapshot<void> 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<void>(
future: Future.delayed(Duration(milliseconds: 2000)),
// 传入Future
builder: (
BuildContext context,
AsyncSnapshot<void> 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<Sonic> with TickerProviderStateMixin {
List<AnimationController> 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<RtcProvider>(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<double> 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()))),
),
),
);
}
}