457 lines
16 KiB
Dart
457 lines
16 KiB
Dart
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/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);
|
|
|
|
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);
|
|
Provider.of<RtmProvider>(context, listen: false).msgFloatingGiftListener =
|
|
_floatingGiftListener;
|
|
_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 dispose() {
|
|
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
|
if (rtmProvider.msgFloatingGiftListener == _floatingGiftListener) {
|
|
rtmProvider.msgFloatingGiftListener = null;
|
|
}
|
|
_clearLuckyGiftComboSessions();
|
|
_giftSeatFlightController.clear();
|
|
_tabController.dispose(); // 释放资源
|
|
_subscription.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
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) {
|
|
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<GiftProvider>(context, listen: false).hideLGiftAnimal) {
|
|
return;
|
|
}
|
|
var giftModel = LGiftModel();
|
|
final giftTab = (msg.gift?.giftTab ?? '').trim();
|
|
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;
|
|
giftModel.isLuckyGift =
|
|
giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name;
|
|
Provider.of<GiftAnimationManager>(
|
|
context,
|
|
listen: false,
|
|
).enqueueGiftAnimation(giftModel);
|
|
|
|
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
|
|
final targetUserId = _resolveGiftTargetUserId(msg);
|
|
final isLuckyGift = _isLuckyGiftMessage(msg);
|
|
if (isLuckyGift) {
|
|
_handleLuckyGiftComboVisuals(msg, giftPhoto, targetUserId);
|
|
return;
|
|
}
|
|
if (_shouldPlaySeatFlightGiftAnimation(msg) && targetUserId != null) {
|
|
_giftSeatFlightController.enqueue(
|
|
RoomGiftSeatFlightRequest(
|
|
imagePath: giftPhoto,
|
|
targetUserId: targetUserId,
|
|
beginSize: 96.w,
|
|
endSize: 28.w,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
bool _isLuckyGiftMessage(Msg msg) {
|
|
final giftTab = (msg.gift?.giftTab ?? '').trim();
|
|
return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name;
|
|
}
|
|
|
|
void _handleLuckyGiftComboVisuals(
|
|
Msg msg,
|
|
String giftPhoto,
|
|
String? targetUserId,
|
|
) {
|
|
final quantity = (msg.number ?? 0).floor();
|
|
if (quantity <= 0) {
|
|
return;
|
|
}
|
|
|
|
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId);
|
|
final session = _luckyGiftComboSessions.putIfAbsent(
|
|
sessionKey,
|
|
() => _LuckyGiftComboSession(),
|
|
);
|
|
session.totalCount += quantity;
|
|
|
|
final highestMilestone =
|
|
SocialChatGiftSystemManager.resolveHighestReachedLuckGiftMilestone(
|
|
session.totalCount,
|
|
);
|
|
if (highestMilestone != null &&
|
|
highestMilestone > session.highestPlayedMilestone &&
|
|
SCGlobalConfig.isLuckGiftSpecialEffects) {
|
|
final effectPath =
|
|
SocialChatGiftSystemManager.resolveLuckGiftComboEffectPath(
|
|
highestMilestone,
|
|
);
|
|
if (effectPath != null && effectPath.isNotEmpty) {
|
|
SCGiftVapSvgaManager().play(effectPath, priority: 200);
|
|
session.highestPlayedMilestone = highestMilestone;
|
|
}
|
|
}
|
|
|
|
if (_shouldPlaySeatFlightGiftAnimation(msg) &&
|
|
targetUserId != null &&
|
|
giftPhoto.isNotEmpty) {
|
|
session.pendingFlightRequest = RoomGiftSeatFlightRequest(
|
|
imagePath: giftPhoto,
|
|
targetUserId: targetUserId,
|
|
beginSize: 96.w,
|
|
endSize: 28.w,
|
|
);
|
|
}
|
|
session.flushTimer?.cancel();
|
|
session.flushTimer = Timer(_luckyGiftComboWindow, () {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
final activeSession = _luckyGiftComboSessions.remove(sessionKey);
|
|
final pendingFlightRequest = activeSession?.pendingFlightRequest;
|
|
activeSession?.dispose();
|
|
if (pendingFlightRequest != null) {
|
|
_giftSeatFlightController.enqueue(pendingFlightRequest);
|
|
}
|
|
});
|
|
}
|
|
|
|
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? flushTimer;
|
|
int totalCount = 0;
|
|
int highestPlayedMilestone = 0;
|
|
RoomGiftSeatFlightRequest? pendingFlightRequest;
|
|
|
|
void dispose() {
|
|
flushTimer?.cancel();
|
|
}
|
|
}
|