chatapp3-flutter/lib/modules/room/voice_room_page.dart
2026-04-18 19:00:19 +08:00

646 lines
21 KiB
Dart

import 'dart:math' as math;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart';
import 'package:provider/provider.dart';
import 'package:yumi/shared/tools/sc_lk_event_bus.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart';
import 'package:yumi/services/gift/gift_animation_manager.dart';
import 'package:yumi/services/gift/gift_system_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
import 'package:yumi/shared/tools/sc_path_utils.dart';
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart';
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_screen.dart';
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart';
import 'package:yumi/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart';
import 'package:yumi/ui_kit/widgets/room/room_head_widget.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:yumi/ui_kit/widgets/room/room_online_user_widget.dart';
import 'package:yumi/ui_kit/widgets/room/room_play_widget.dart';
import 'package:yumi/shared/data_sources/models/enum/sc_gift_type.dart';
import '../../ui_kit/components/sc_float_ichart.dart';
import '../../ui_kit/widgets/room/seat/room_seat_widget.dart';
import 'chat/all/all_chat_page.dart';
import 'chat/chat/chat_page.dart';
import 'chat/gift/gift_chat_page.dart';
///语聊房
class VoiceRoomPage extends StatefulWidget {
const VoiceRoomPage({super.key});
@override
State<VoiceRoomPage> createState() => _VoiceRoomPageState();
}
class _VoiceRoomPageState extends State<VoiceRoomPage>
with SingleTickerProviderStateMixin {
static const Duration _luckyGiftComboWindow = Duration(seconds: 3);
static const Duration _luckyGiftQueueDrainWindow = Duration(seconds: 3);
static const int _maxLuckyGiftTrackedAnimations = 5;
static const Duration _giftAnimationSessionWindow = _luckyGiftComboWindow;
static const Duration _giftAnimationQueueDrainWindow =
_luckyGiftQueueDrainWindow;
static const int _maxTrackedGiftAnimations = _maxLuckyGiftTrackedAnimations;
late TabController _tabController;
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
final List<Widget> _tabs = [];
late StreamSubscription _subscription;
final RoomGiftSeatFlightController _giftSeatFlightController =
RoomGiftSeatFlightController();
final Map<String, _LuckyGiftComboSession> _luckyGiftComboSessions =
<String, _LuckyGiftComboSession>{};
@override
void initState() {
super.initState();
_tabController = TabController(length: _pages.length, vsync: this);
_enableRoomVisualEffects();
_tabController.addListener(() {}); // 监听切换
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((
event,
) {
if (mounted) {
Provider.of<GiftProvider>(context, listen: false).clearAllGiftData();
Provider.of<GiftProvider>(
context,
listen: false,
).toggleGiftAnimationVisibility(false);
_clearLuckyGiftComboSessions();
_giftSeatFlightController.clear();
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_ensureRoomVisualEffectsEnabled();
}
@override
void dispose() {
_suspendRoomVisualEffects();
_tabController.dispose(); // 释放资源
_subscription.cancel();
super.dispose();
}
void _enableRoomVisualEffects() {
Provider.of<RtcProvider>(
context,
listen: false,
).setRoomVisualEffectsEnabled(true);
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
rtmProvider.msgFloatingGiftListener = _floatingGiftListener;
rtmProvider.msgLuckyGiftRewardTickerListener =
_luckyGiftRewardTickerListener;
}
void _ensureRoomVisualEffectsEnabled() {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
if (rtcProvider.currenRoom == null ||
rtcProvider.roomVisualEffectsEnabled) {
return;
}
_enableRoomVisualEffects();
}
void _suspendRoomVisualEffects() {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
rtcProvider.setRoomVisualEffectsEnabled(false);
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
if (rtmProvider.msgFloatingGiftListener == _floatingGiftListener) {
rtmProvider.msgFloatingGiftListener = null;
}
if (rtmProvider.msgLuckyGiftRewardTickerListener ==
_luckyGiftRewardTickerListener) {
rtmProvider.msgLuckyGiftRewardTickerListener = null;
}
RoomEntranceHelper.clearQueue();
_clearLuckyGiftComboSessions();
_giftSeatFlightController.clear();
SCGiftVapSvgaManager().stopPlayback();
}
bool roomThemeBackActi(JoinRoomRes? room) {
if (room?.roomProps?.roomTheme != null) {
if (room?.roomProps?.roomTheme?.themeBack != null &&
room!.roomProps!.roomTheme!.themeBack!.isNotEmpty) {
if ((room.roomProps?.roomTheme?.expireTime ?? 0) >
DateTime.now().millisecondsSinceEpoch) {
return true;
}
}
}
return false;
}
@override
Widget build(BuildContext context) {
_tabs.clear();
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.all));
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.chat));
_tabs.add(Tab(text: SCAppLocalizations.of(context)!.gift));
return PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (!didPop) {
_suspendRoomVisualEffects();
SCFloatIchart().show();
SCNavigatorUtils.goBack(context);
}
},
child: Scaffold(
backgroundColor:
SCGlobalConfig.businessLogicStrategy.getVoiceRoomBackgroundColor(),
resizeToAvoidBottomInset: false,
body: SafeArea(
top: false,
child: Stack(
children: [
Consumer<RtcProvider>(
builder: (context, ref, child) {
return roomThemeBackActi(ref.currenRoom)
? netImage(
url:
ref.currenRoom?.roomProps?.roomTheme?.themeBack ??
"",
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight,
noDefaultImg: true,
fit: BoxFit.cover,
)
: Image.asset(
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomDefaultBackgroundImage(),
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight,
fit: BoxFit.cover,
);
},
),
Column(
children: [
SizedBox(height: ScreenUtil().setWidth(42)),
RoomHeadWidget(),
SizedBox(height: ScreenUtil().setWidth(5)),
RoomOnlineUserWidget(),
RoomSeatWidget(),
SizedBox(height: 2.w),
Expanded(
child: Stack(
children: [
Column(
children: [_buildChatView(), RoomBottomWidget()],
),
LGiftAnimalPage(),
Transform.translate(
offset: Offset(0, -20),
child: RoomAnimationQueueScreen(),
),
],
),
),
],
),
// _buildPlayViews(),
///幸运礼物中奖动画
LuckGiftNomorAnimWidget(),
],
),
),
),
);
}
///消息
Widget _buildChatView() {
return Expanded(
child: Stack(
alignment: AlignmentDirectional.bottomEnd,
children: [
Column(
children: [
TabBar(
tabAlignment: TabAlignment.start,
labelPadding: SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabLabelPadding()
.copyWith(
left:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabLabelPadding()
.left *
ScreenUtil().setWidth(1),
right:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabLabelPadding()
.right *
ScreenUtil().setWidth(1),
),
labelColor:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabLabelColor(),
isScrollable: true,
indicator: BoxDecoration(),
unselectedLabelColor:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabUnselectedLabelColor(),
labelStyle: SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabLabelStyle()
.copyWith(
fontSize:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabLabelStyle()
.fontSize! *
ScreenUtil().setSp(1),
),
unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabUnselectedLabelStyle()
.copyWith(
fontSize:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabUnselectedLabelStyle()
.fontSize! *
ScreenUtil().setSp(1),
),
indicatorColor:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabIndicatorColor(),
dividerColor:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomTabDividerColor(),
controller: _tabController,
tabs: _tabs,
),
Expanded(
child: Container(
margin: SCGlobalConfig.businessLogicStrategy
.getVoiceRoomChatContainerMargin()
.copyWith(
end:
SCGlobalConfig.businessLogicStrategy
.getVoiceRoomChatContainerMargin()
.end *
ScreenUtil().setWidth(1),
),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: TabBarView(
controller: _tabController,
children: _pages,
),
),
),
),
],
),
RoomPlayWidget(),
],
),
);
}
///礼物上飘动画
_floatingGiftListener(Msg msg) {
if (!Provider.of<RtcProvider>(
context,
listen: false,
).shouldShowRoomVisualEffects) {
return;
}
if (Provider.of<GiftProvider>(context, listen: false).hideLGiftAnimal) {
return;
}
var giftModel = LGiftModel();
giftModel.labelId = "${msg.gift?.id}${msg.user?.id}${msg.toUser?.id}";
giftModel.sendUserName = msg.user?.userNickname ?? "";
giftModel.sendToUserName = msg.toUser?.userNickname ?? "";
giftModel.sendUserPic = msg.user?.userAvatar ?? "";
giftModel.giftPic = msg.gift?.giftPhoto ?? "";
giftModel.giftCount = msg.number ?? 0;
Provider.of<GiftAnimationManager>(
context,
listen: false,
).enqueueGiftAnimation(giftModel);
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
final targetUserId = _resolveGiftTargetUserId(msg);
if (_supportsComboMilestoneEffects(msg)) {
_handleComboMilestoneVisuals(msg, targetUserId);
}
final isLuckyGift = _isLuckyGiftMessage(msg);
if (isLuckyGift) {
_handleLuckyGiftComboVisuals(msg, giftPhoto, targetUserId);
return;
}
_handleStandardGiftComboVisuals(msg, giftPhoto, targetUserId);
}
void _luckyGiftRewardTickerListener(Msg msg) {
if (!Provider.of<RtcProvider>(
context,
listen: false,
).shouldShowRoomVisualEffects) {
return;
}
if (Provider.of<GiftProvider>(context, listen: false).hideLGiftAnimal) {
return;
}
final awardAmount = msg.awardAmount ?? 0;
if (awardAmount <= 0) {
return;
}
final giftModel = LGiftModel();
giftModel.labelId = "${msg.gift?.id}${msg.user?.id}${msg.toUser?.id}";
giftModel.sendUserName = msg.user?.userNickname ?? "";
giftModel.sendToUserName = msg.toUser?.userNickname ?? "";
giftModel.sendUserPic = msg.user?.userAvatar ?? "";
giftModel.giftPic = msg.gift?.giftPhoto ?? "";
giftModel.giftCount = 0;
giftModel.showLuckyRewardFrame = true;
giftModel.rewardAmountText = _formatLuckyRewardAmount(awardAmount);
Provider.of<GiftAnimationManager>(
context,
listen: false,
).enqueueGiftAnimation(giftModel);
}
String _formatLuckyRewardAmount(num awardAmount) {
if (awardAmount > 9999) {
return "${(awardAmount / 1000).toStringAsFixed(0)}k";
}
if (awardAmount % 1 == 0) {
return awardAmount.toInt().toString();
}
return awardAmount.toString();
}
bool _isLuckyGiftMessage(Msg msg) {
final giftTab = (msg.gift?.giftTab ?? '').trim();
return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name;
}
bool _supportsComboMilestoneEffects(Msg msg) {
return SocialChatGiftSystemManager.supportsComboMilestoneEffects(msg.gift);
}
bool _shouldPlayComboMilestoneEffect(Msg msg) {
if (!_supportsComboMilestoneEffects(msg)) {
return false;
}
return SCGlobalConfig.isLuckGiftSpecialEffects;
}
void _handleComboMilestoneVisuals(Msg msg, String? targetUserId) {
final quantity = (msg.number ?? 0).floor();
if (quantity <= 0) {
return;
}
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId);
final session = _luckyGiftComboSessions.putIfAbsent(
sessionKey,
() => _LuckyGiftComboSession(),
);
session.endTimer?.cancel();
session.clearQueueTimer?.cancel();
session.totalCount += quantity;
final highestMilestone =
SocialChatGiftSystemManager.resolveHighestReachedComboMilestone(
session.totalCount,
);
if (highestMilestone != null &&
highestMilestone > session.highestPlayedMilestone &&
_shouldPlayComboMilestoneEffect(msg)) {
final effectPath =
SocialChatGiftSystemManager.resolveComboMilestoneEffectPath(
highestMilestone,
);
if (effectPath != null && effectPath.isNotEmpty) {
SCGiftVapSvgaManager().play(effectPath, priority: 200);
session.highestPlayedMilestone = highestMilestone;
}
}
}
void _handleStandardGiftComboVisuals(
Msg msg,
String giftPhoto,
String? targetUserId,
) {
if ((msg.number ?? 0) <= 0) {
return;
}
if (!_shouldPlaySeatFlightGiftAnimation(msg) ||
targetUserId == null ||
giftPhoto.isEmpty) {
return;
}
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId);
final session = _luckyGiftComboSessions.putIfAbsent(
sessionKey,
() => _LuckyGiftComboSession(),
);
session.endTimer?.cancel();
session.clearQueueTimer?.cancel();
_enqueueTrackedSeatFlightAnimations(
sessionKey: sessionKey,
giftPhoto: giftPhoto,
targetUserId: targetUserId,
animationCount: _resolveTrackedAnimationCount(msg),
);
_scheduleGiftAnimationSessionEnd(sessionKey);
}
void _handleLuckyGiftComboVisuals(
Msg msg,
String giftPhoto,
String? targetUserId,
) {
if ((msg.number ?? 0) <= 0) {
return;
}
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId);
final session = _luckyGiftComboSessions[sessionKey];
if (session == null) {
return;
}
session.endTimer?.cancel();
session.clearQueueTimer?.cancel();
if (_shouldPlaySeatFlightGiftAnimation(msg) &&
targetUserId != null &&
giftPhoto.isNotEmpty) {
_enqueueTrackedSeatFlightAnimations(
sessionKey: sessionKey,
giftPhoto: giftPhoto,
targetUserId: targetUserId,
animationCount: _resolveTrackedAnimationCount(msg),
);
}
_scheduleGiftAnimationSessionEnd(sessionKey);
}
int _resolveTrackedAnimationCount(Msg msg) {
return math.max(msg.customAnimationCount ?? 1, 1);
}
void _enqueueTrackedSeatFlightAnimations({
required String sessionKey,
required String giftPhoto,
required String targetUserId,
required int animationCount,
}) {
final normalizedAnimationCount = math.max(animationCount, 1);
final cappedAnimationCount = math.min(
normalizedAnimationCount,
_maxTrackedGiftAnimations,
);
for (var index = 0; index < cappedAnimationCount; index += 1) {
_giftSeatFlightController.enqueueLimited(
RoomGiftSeatFlightRequest(
imagePath: giftPhoto,
targetUserId: targetUserId,
beginSize: 96.w,
endSize: 28.w,
queueTag: sessionKey,
),
maxTrackedRequests: _maxTrackedGiftAnimations,
);
}
}
void _scheduleGiftAnimationSessionEnd(String sessionKey) {
final session = _luckyGiftComboSessions[sessionKey];
if (session == null) {
return;
}
session.endTimer?.cancel();
session.endTimer = Timer(_giftAnimationSessionWindow, () {
if (!mounted) {
return;
}
final activeSession = _luckyGiftComboSessions[sessionKey];
if (activeSession == null) {
return;
}
if (!_giftSeatFlightController.hasTrackedRequests(sessionKey)) {
activeSession.dispose();
_luckyGiftComboSessions.remove(sessionKey);
return;
}
activeSession.clearQueueTimer?.cancel();
activeSession.clearQueueTimer = Timer(_giftAnimationQueueDrainWindow, () {
_giftSeatFlightController.clearQueuedRequests(sessionKey);
final expiredSession = _luckyGiftComboSessions.remove(sessionKey);
expiredSession?.dispose();
});
});
}
String _buildLuckyGiftComboSessionKey(Msg msg, String? targetUserId) {
final senderId = (msg.user?.id ?? '').trim();
final giftId = (msg.gift?.id ?? '').trim();
return '$giftId|$senderId|${targetUserId ?? ""}';
}
void _clearLuckyGiftComboSessions() {
for (final session in _luckyGiftComboSessions.values) {
session.dispose();
}
_luckyGiftComboSessions.clear();
}
bool _shouldPlaySeatFlightGiftAnimation(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);
if (_isAnimatedGiftResource(giftPhotoExt)) {
return false;
}
final giftSourceUrl = (gift.giftSourceUrl ?? "").trim();
final sourceExt = _normalizedGiftResourceExtension(giftSourceUrl);
return !_isAnimatedGiftResource(sourceExt);
}
bool _isAnimatedGiftResource(String extension) {
return extension == ".svga" || extension == ".mp4" || extension == ".vap";
}
String _normalizedGiftResourceExtension(String resource) {
final value = resource.trim();
if (value.isEmpty) {
return "";
}
final uri = Uri.tryParse(value);
if (uri != null && ((uri.scheme.isNotEmpty) || (uri.host.isNotEmpty))) {
return SCPathUtils.getFileExtension(uri.path).toLowerCase();
}
final normalizedValue = value.split("?").first.split("#").first;
return SCPathUtils.getFileExtension(normalizedValue).toLowerCase();
}
String? _resolveGiftTargetUserId(Msg msg) {
final directUserId = (msg.toUser?.id ?? "").trim();
if (directUserId.isNotEmpty) {
return directUserId;
}
final targetAccount = (msg.toUser?.account ?? "").trim();
if (targetAccount.isEmpty) {
return null;
}
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
for (final micRes in rtcProvider.roomWheatMap.values) {
if ((micRes.user?.account ?? "").trim() == targetAccount) {
final userId = (micRes.user?.id ?? "").trim();
if (userId.isNotEmpty) {
return userId;
}
}
}
return null;
}
}
class _LuckyGiftComboSession {
Timer? endTimer;
Timer? clearQueueTimer;
int totalCount = 0;
int highestPlayedMilestone = 0;
void dispose() {
endTimer?.cancel();
clearQueueTimer?.cancel();
}
}