diff --git a/ios/Podfile b/ios/Podfile index 0cbf306..5aa1473 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -27,7 +27,7 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_ios_podfile_setup -platform :ios, '13.0' +platform :ios, '14.0' # 在 target 'Runner' do 之前添加 install! 'cocoapods', :deterministic_uuids => false @@ -37,11 +37,6 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - - # 强制使用较新的 AgoraInfra_iOS 版本 - pod 'AgoraInfra_iOS', '1.2.13.1' - - target 'RunnerTests' do inherit! :search_paths @@ -53,7 +48,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' end end end diff --git a/lib/controller/discover/room_controller.dart b/lib/controller/discover/room_controller.dart index b952297..d5751d6 100644 --- a/lib/controller/discover/room_controller.dart +++ b/lib/controller/discover/room_controller.dart @@ -239,8 +239,12 @@ class RoomController extends GetxController with WidgetsBindingObserver { final newDetail = RtcChannelDetail( channelId: rtcChannelDetail.value!.channelId, anchorInfo: rtcChannelDetail.value!.anchorInfo, - maleInfo: role == CurrentRole.maleAudience ? userInfo : null, - femaleInfo: role == CurrentRole.femaleAudience ? userInfo : null, + maleInfo: role == CurrentRole.maleAudience + ? userInfo + : rtcChannelDetail.value?.maleInfo, + femaleInfo: role == CurrentRole.femaleAudience + ? userInfo + : rtcChannelDetail.value?.femaleInfo, ); rtcChannelDetail.value = newDetail; isLive.value = true; @@ -322,15 +326,27 @@ class RoomController extends GetxController with WidgetsBindingObserver { } Future leaveChannel() async { - // 如果是主播,发送结束直播消息 + // 如果是主播,先销毁 RTC 频道,然后发送结束直播消息 if (currentRole == CurrentRole.broadcaster) { - final channelId = RTCManager.instance.currentChannelId; - if (channelId != null && channelId.isNotEmpty) { - await RTMManager.instance.publishChannelMessage( - channelName: channelId, - message: json.encode({'type': 'end_live'}), - ); + try { + // 先调用销毁 RTC 频道 API + final destroyResponse = await _networkService.rtcApi + .destroyRtcChannel(); + if (destroyResponse.data.isSuccess) { + // 然后发送结束直播消息 + final channelId = RTCManager.instance.currentChannelId; + if (channelId != null && channelId.isNotEmpty) { + await RTMManager.instance.publishChannelMessage( + channelName: channelId, + message: json.encode({'type': 'end_live'}), + ); + } + } + } catch (e) { + print('❌ 销毁 RTC 频道异常: $e'); } + + } isLive.value = false; @@ -358,9 +374,76 @@ class RoomController extends GetxController with WidgetsBindingObserver { } } + /// 赠送礼物 + Future sendGift({ + required GiftProductModel gift, + String? targetUserId, + }) async { + try { + // 添加到本地播放队列 + final svgaManager = SvgaPlayerManager.instance; + svgaManager.addToQueue( + SvgaAnimationItem( + svgaFile: gift.svgaFile, + targetUserId: targetUserId, + senderUserId: GlobalData().userId ?? GlobalData().userData?.id, + giftProductId: gift.productId, + ), + ); + print('✅ 礼物已添加到播放队列: ${gift.productTitle}'); + + // 发送 RTM 消息通知其他用户 + final channelId = RTCManager.instance.currentChannelId; + if (channelId != null && channelId.isNotEmpty) { + final messageData = { + 'type': 'gift', + 'svgaFile': gift.svgaFile, + 'giftProductId': gift.productId, + 'targetUserId': targetUserId, + 'senderUserId': GlobalData().userId ?? GlobalData().userData?.id, + 'senderNickName': GlobalData().userData?.nickName ?? '', + }; + + await RTMManager.instance.publishChannelMessage( + channelName: channelId, + message: json.encode(messageData), + ); + print('✅ 礼物消息已发送: ${gift.productTitle}'); + } + } catch (e) { + print('❌ 发送礼物失败: $e'); + SmartDialog.showToast('发送礼物失败'); + } + } + /// 接收RTC消息 Future receiveRTCMessage(Map message) async { - if (message['type'] == 'join_chat') { + if (message['type'] == 'gift') { + // 处理礼物消息 + try { + final svgaFile = message['svgaFile']?.toString() ?? ''; + final giftProductId = message['giftProductId']?.toString(); + final targetUserId = message['targetUserId']?.toString(); + final senderUserId = message['senderUserId']?.toString(); + final senderNickName = message['senderNickName']?.toString() ?? ''; + + if (svgaFile.isNotEmpty) { + // 添加到播放队列 + final svgaManager = SvgaPlayerManager.instance; + svgaManager.addToQueue( + SvgaAnimationItem( + svgaFile: svgaFile, + targetUserId: targetUserId, + senderUserId: senderUserId, + giftProductId: giftProductId, + ), + ); + print('✅ 收到礼物消息,已添加到播放队列: $senderNickName 赠送了礼物'); + } + } catch (e) { + print('❌ 处理礼物消息失败: $e'); + } + } else if (message['type'] == 'join_chat') { final response = await _networkService.rtcApi .getDatingRtcChannelUserDetail( rtcChannel.value!.channelId, diff --git a/lib/controller/discover/svga_player_manager.dart b/lib/controller/discover/svga_player_manager.dart new file mode 100644 index 0000000..ffb9ede --- /dev/null +++ b/lib/controller/discover/svga_player_manager.dart @@ -0,0 +1,86 @@ +import 'dart:collection'; +import 'package:flutter/material.dart'; +import 'package:flutter_svga/flutter_svga.dart'; +import 'package:get/get.dart'; + +/// SVGA 动画项 +class SvgaAnimationItem { + final String svgaFile; + final String? targetUserId; // 接收礼物的用户ID + final String? senderUserId; // 发送礼物的用户ID + final String? giftProductId; // 礼物产品ID + + SvgaAnimationItem({ + required this.svgaFile, + this.targetUserId, + this.senderUserId, + this.giftProductId, + }); +} + +/// SVGA 动画播放队列管理器 +/// 注意:由于 SVGAAnimationController 需要 vsync,实际播放需要在 Widget 中完成 +/// 这个管理器只负责管理队列,实际的播放需要通过回调通知外部 Widget +class SvgaPlayerManager extends GetxController { + static SvgaPlayerManager? _instance; + static SvgaPlayerManager get instance { + _instance ??= Get.put(SvgaPlayerManager()); + return _instance!; + } + + final Queue _animationQueue = Queue(); + final Rx currentItem = Rx(null); + final RxBool isPlaying = false.obs; + + /// 添加动画到队列 + void addToQueue(SvgaAnimationItem item) { + _animationQueue.add(item); + _playNext(); + } + + /// 播放下一个动画 + void _playNext() { + if (isPlaying.value || _animationQueue.isEmpty) { + return; + } + + final item = _animationQueue.removeFirst(); + currentItem.value = item; + isPlaying.value = true; + print('✅ SVGA 动画已添加到播放队列: ${item.svgaFile}'); + } + + /// 标记当前动画播放完成 + void onAnimationFinished() { + print('✅ SVGA 动画播放完成'); + isPlaying.value = false; + currentItem.value = null; + // 播放下一个 + _playNext(); + } + + /// 标记当前动画播放失败 + void onAnimationError(String error) { + print('❌ SVGA 动画播放失败: $error'); + isPlaying.value = false; + currentItem.value = null; + // 继续播放下一个 + _playNext(); + } + + /// 停止当前播放 + void stop() { + isPlaying.value = false; + currentItem.value = null; + } + + /// 清空队列 + void clearQueue() { + stop(); + _animationQueue.clear(); + } + + /// 获取队列长度 + int get queueLength => _animationQueue.length; +} + diff --git a/lib/controller/mine/mine_controller.dart b/lib/controller/mine/mine_controller.dart index f6c8a59..184e4ae 100644 --- a/lib/controller/mine/mine_controller.dart +++ b/lib/controller/mine/mine_controller.dart @@ -23,6 +23,7 @@ class MineController extends GetxController { {"icon": Assets.imagesWallet, "title": "我的钱包", "subTitle": "提现无门槛", "path": () => MyWalletPage()}, {"icon": Assets.imagesShop, "title": "商城中心", "subTitle": "不定期更新商品", "path": () => Null}, {"icon": Assets.imagesCert, "title": "认证中心", "subTitle": "未认证", "path": () => AuthCenterPage()}, + {"icon": Assets.imagesShop, "title": "红娘等级", "subTitle": "实习红娘", "path": () => Null}, ].obs; List settingList = [ diff --git a/lib/controller/mine/rose_controller.dart b/lib/controller/mine/rose_controller.dart index 15a576f..f5f954f 100644 --- a/lib/controller/mine/rose_controller.dart +++ b/lib/controller/mine/rose_controller.dart @@ -83,6 +83,7 @@ class RoseController extends GetxController { } else { fluwx.open(target: MiniProgram( username: 'gh_9ea8d46add6f', + miniProgramType: WXMiniProgramType.preview, path:"pages/index/index?amount=0.01&paymentOrderId=${data!.paymentOrderId}&url=match-fee" )); } diff --git a/lib/model/home/marriage_data.dart b/lib/model/home/marriage_data.dart index 1405b89..6a8acf9 100644 --- a/lib/model/home/marriage_data.dart +++ b/lib/model/home/marriage_data.dart @@ -1,4 +1,6 @@ // 数据模型类 - 根据真实API返回格式调整 +import 'user_info_data.dart'; + class MarriageData { final String miId; final String userId; @@ -66,6 +68,32 @@ class MarriageData { photoList: (json['photoList'] as List?)?.map((e) => PhotoItem.fromJson(e as Map)).toList() ?? [], ); } + + /// 从 UserInfoData 转换为 MarriageData + factory MarriageData.fromUserInfoData(UserInfoData userInfo) { + return MarriageData( + miId: userInfo.miId ?? '', + userId: userInfo.userId ?? '', + profilePhoto: userInfo.profilePhoto ?? '', + nickName: userInfo.nickName ?? '', + isRealNameCertified: userInfo.identityCard != null && userInfo.identityCard!.isNotEmpty, + birthYear: userInfo.birthYear ?? '', + birthDate: userInfo.birthDate ?? '', + age: userInfo.age?.toInt() ?? 0, + provinceCode: userInfo.provinceCode?.toInt() ?? 0, + provinceName: userInfo.provinceName ?? '', + cityCode: userInfo.cityCode?.toInt() ?? 0, + cityName: userInfo.cityName ?? '', + districtCode: userInfo.districtCode?.toInt() ?? 0, + districtName: userInfo.districtName ?? '', + describeInfo: userInfo.describeInfo ?? '', + createTime: userInfo.createTime ?? '', + photoList: (userInfo.photoList ?? []).map((photo) => PhotoItem( + photoUrl: photo.photoUrl ?? '', + auditStatus: photo.auditStatus, + )).toList(), + ); + } } // 照片项数据模型 diff --git a/lib/network/api_urls.dart b/lib/network/api_urls.dart index cb080a0..3e6707d 100644 --- a/lib/network/api_urls.dart +++ b/lib/network/api_urls.dart @@ -56,6 +56,8 @@ class ApiUrls { 'dating-agency-chat-audio/user/enable/rtc-channel-user/audio'; static const String disconnectRtcChannel = 'dating-agency-chat-audio/user/disconnect/rtc-channel'; + static const String destroyRtcChannel = + 'dating-agency-chat-audio/user/destroy/rtc-channel'; static const String getRtcChannelPage = 'dating-agency-chat-audio/user/page/rtc-channel'; static const String listGiftProduct = diff --git a/lib/network/rtc_api.dart b/lib/network/rtc_api.dart index 6ec1931..7c2e928 100644 --- a/lib/network/rtc_api.dart +++ b/lib/network/rtc_api.dart @@ -59,6 +59,10 @@ abstract class RtcApi { @Body() Map data, ); + /// 销毁 RTC 频道 + @POST(ApiUrls.destroyRtcChannel) + Future>> destroyRtcChannel(); + /// 获取 RTC 频道分页列表 @GET(ApiUrls.getRtcChannelPage) Future>>> getRtcChannelPage(); diff --git a/lib/network/rtc_api.g.dart b/lib/network/rtc_api.g.dart index e9ee003..52bbfcb 100644 --- a/lib/network/rtc_api.g.dart +++ b/lib/network/rtc_api.g.dart @@ -287,6 +287,37 @@ class _RtcApi implements RtcApi { return httpResponse; } + @override + Future>> destroyRtcChannel() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>>( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-chat-audio/user/destroy/rtc-channel', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => json as dynamic, + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + @override Future>>> getRtcChannelPage() async { diff --git a/lib/pages/discover/live_room_page.dart b/lib/pages/discover/live_room_page.dart index 29c566c..b4fb318 100644 --- a/lib/pages/discover/live_room_page.dart +++ b/lib/pages/discover/live_room_page.dart @@ -1,6 +1,5 @@ import 'package:dating_touchme_app/controller/discover/room_controller.dart'; import 'package:dating_touchme_app/controller/overlay_controller.dart'; -import 'package:dating_touchme_app/generated/assets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; @@ -13,6 +12,7 @@ import 'package:dating_touchme_app/widget/live/live_room_notice_chat_panel.dart' import 'package:dating_touchme_app/widget/live/live_room_action_bar.dart'; import 'package:dating_touchme_app/widget/live/live_gift_popup.dart'; import 'package:dating_touchme_app/widget/live/live_recharge_popup.dart'; +import 'package:dating_touchme_app/widget/live/svga_player_widget.dart'; class LiveRoomPage extends StatefulWidget { final int id; @@ -207,6 +207,8 @@ class _LiveRoomPageState extends State { ], ), ), + // SVGA 动画播放组件 + const SvgaPlayerWidget(), ], ), ), diff --git a/lib/pages/home/user_information_page.dart b/lib/pages/home/user_information_page.dart index c1342fb..68cd53c 100644 --- a/lib/pages/home/user_information_page.dart +++ b/lib/pages/home/user_information_page.dart @@ -1,5 +1,4 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:dating_touchme_app/controller/global.dart'; import 'package:dating_touchme_app/controller/home/user_information_controller.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/model/home/marriage_data.dart'; @@ -383,10 +382,15 @@ class UserInformationPage extends StatelessWidget { backgroundColor: Color(0xC3333333), ), onTap: (){ - // Get.to(() => ChatPage( - // userId: controller.userData.value.userId ?? "", - // userData: widget.userData, - // )); + final userInfo = controller.userData.value; + if (userInfo.userId != null && userInfo.userId!.isNotEmpty) { + // 使用工厂方法将 UserInfoData 转换为 MarriageData + final marriageData = MarriageData.fromUserInfoData(userInfo); + Get.to(() => ChatPage( + userId: userInfo.userId ?? "", + userData: marriageData, + )); + } }, ), const SizedBox(width: 10), diff --git a/lib/pages/mine/mine_page.dart b/lib/pages/mine/mine_page.dart index 9ff3512..cbfa843 100644 --- a/lib/pages/mine/mine_page.dart +++ b/lib/pages/mine/mine_page.dart @@ -99,12 +99,19 @@ class _MinePageState extends State with AutomaticKeepAliveClientMixin{ children: [ Row( children: [ - Text( - "${controller.userData.value?.nickName ?? ""}", - style: TextStyle( - fontSize: 18.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w700 + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 120.w, // 最大宽度 + ), + child: Text( + "${controller.userData.value?.nickName ?? ""}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w700 + ), ), ), SizedBox(width: 8.w,), diff --git a/lib/widget/live/live_gift_popup.dart b/lib/widget/live/live_gift_popup.dart index 0da1d71..2e4daab 100644 --- a/lib/widget/live/live_gift_popup.dart +++ b/lib/widget/live/live_gift_popup.dart @@ -2,13 +2,13 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dating_touchme_app/controller/discover/room_controller.dart'; import 'package:dating_touchme_app/controller/global.dart'; import 'package:dating_touchme_app/generated/assets.dart'; +import 'package:dating_touchme_app/model/live/gift_product_model.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart'; import 'package:dating_touchme_app/widget/live/live_room_gift_item.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_swiper_null_safety/flutter_swiper_null_safety.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; class LiveGiftPopup extends StatefulWidget { const LiveGiftPopup({ @@ -29,34 +29,76 @@ class LiveGiftPopup extends StatefulWidget { } class _LiveGiftPopupState extends State { - // 选中的用户ID集合 - final Set _selectedUserIds = {}; + // 选中的用户ID(单选) + String? _selectedUserId; - // 切换用户选中状态 + @override + void initState() { + super.initState(); + // 默认选择第一个礼物 + if (widget.giftList.isNotEmpty && widget.activeGift.value == null) { + widget.activeGift.value = 0; + } + } + + // 切换用户选中状态(单选模式) void _toggleUserSelection(String userId) { setState(() { - if (_selectedUserIds.contains(userId)) { - _selectedUserIds.remove(userId); + if (_selectedUserId == userId) { + // 如果点击的是已选中的用户,则取消选中 + _selectedUserId = null; } else { - _selectedUserIds.add(userId); + // 否则选中该用户(自动取消之前的选中) + _selectedUserId = userId; } }); } - // 全选/取消全选 - void _toggleSelectAll(List users) { - setState(() { - if (_selectedUserIds.length == users.length) { - // 如果已全选,则取消全选 - _selectedUserIds.clear(); - } else { - // 否则全选 - _selectedUserIds.clear(); - for (var user in users) { - _selectedUserIds.add(user.userId); - } - } - }); + // 处理赠送礼物 + Future _handleSendGift() async { + // 检查是否选中了礼物 + final activeIndex = widget.activeGift.value; + if (activeIndex == null || + activeIndex < 0 || + activeIndex >= widget.giftList.length) { + SmartDialog.showToast('请先选择礼物'); + return; + } + + // 检查是否选中了接收用户 + if (_selectedUserId == null || _selectedUserId!.isEmpty) { + SmartDialog.showToast('请先选择接收礼物的用户'); + return; + } + + // 获取选中的礼物 + final giftItem = widget.giftList[activeIndex]; + GiftProductModel? gift; + + if (giftItem is GiftProductModel) { + gift = giftItem; + } else if (giftItem is Map) { + // 如果是 Map,需要从 RoomController 的 giftProducts 中查找 + SmartDialog.showToast('礼物数据格式错误'); + return; + } else { + SmartDialog.showToast('礼物数据格式错误'); + return; + } + + // 获取 RoomController + final roomController = Get.isRegistered() + ? Get.find() + : null; + + if (roomController == null) { + SmartDialog.showToast('房间控制器未初始化'); + return; + } + + // 发送礼物 + await roomController.sendGift(gift: gift, targetUserId: _selectedUserId); + SmartDialog.dismiss(); } @override @@ -141,7 +183,7 @@ class _LiveGiftPopupState extends State { ...displayUsers.asMap().entries.map((entry) { final index = entry.key; final user = entry.value; - final isSelected = _selectedUserIds.contains(user.userId); + final isSelected = _selectedUserId == user.userId; return GestureDetector( onTap: () => _toggleUserSelection(user.userId), child: Padding( @@ -228,27 +270,6 @@ class _LiveGiftPopupState extends State { }), ], ), - // 如果列表为空,不显示全选按钮 - if (displayUsers.isNotEmpty) - GestureDetector( - onTap: () => _toggleSelectAll(displayUsers), - child: Container( - width: 63.w, - height: 30.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(30.w)), - color: const Color.fromRGBO(117, 98, 249, 1), - ), - child: Center( - child: Text( - _selectedUserIds.length == displayUsers.length - ? "取消全选" - : "全选", - style: TextStyle(fontSize: 13.w, color: Colors.white), - ), - ), - ), - ), ], ), ); @@ -294,52 +315,25 @@ class _LiveGiftPopupState extends State { ); } - // 计算需要多少页,每页显示8个礼物(2行4列) - final itemsPerPage = 8; - final totalPages = (widget.giftList.length / itemsPerPage).ceil(); - return Expanded( child: ValueListenableBuilder( valueListenable: widget.activeGift, builder: (context, active, _) { - return Swiper( - autoplay: false, - itemCount: totalPages, - loop: false, - pagination: totalPages > 1 - ? const SwiperPagination( - alignment: Alignment.bottomCenter, - builder: TDSwiperDotsPagination( - color: Color.fromRGBO(144, 144, 144, 1), - activeColor: Color.fromRGBO(77, 77, 77, 1), - ), - ) - : null, - itemBuilder: (context, pageIndex) { - final startIndex = pageIndex * itemsPerPage; - final endIndex = (startIndex + itemsPerPage).clamp( - 0, - widget.giftList.length, - ); - final pageItems = widget.giftList.sublist(startIndex, endIndex); - - return Align( - alignment: Alignment.topCenter, - child: Wrap( - spacing: 7.w, - runSpacing: 7.w, - children: [ - ...pageItems.asMap().entries.map((entry) { - final globalIndex = startIndex + entry.key; - return LiveRoomGiftItem( - item: entry.value, - active: active ?? 0, - index: globalIndex, - changeActive: widget.changeActive, - ); - }), - ], - ), + return GridView.builder( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, // 每行4个 + crossAxisSpacing: 7.w, + mainAxisSpacing: 7.w, + childAspectRatio: 0.85, // 调整宽高比 + ), + itemCount: widget.giftList.length, + itemBuilder: (context, index) { + return LiveRoomGiftItem( + item: widget.giftList[index], + active: active ?? 0, + index: index, + changeActive: widget.changeActive, ); }, ); @@ -371,49 +365,27 @@ class _LiveGiftPopupState extends State { builder: (context, num, _) { return Row( children: [ - _buildAdjustButton( - label: "-", - enabled: num > 1, - onTap: () { - if (widget.giftNum.value <= 1) return; - widget.giftNum.value -= 1; - }, - ), - SizedBox( - width: 23.w, - child: Center( - child: Text( - "$num", - style: TextStyle(fontSize: 13.w, color: Colors.white), + GestureDetector( + onTap: () => _handleSendGift(), + child: Container( + width: 63.w, + height: 30.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(30.w)), + gradient: const LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Color.fromRGBO(61, 138, 224, 1), + Color.fromRGBO(131, 89, 255, 1), + ], + ), ), - ), - ), - _buildAdjustButton( - label: "+", - enabled: true, - onTap: () { - widget.giftNum.value += 1; - }, - ), - SizedBox(width: 9.w), - Container( - width: 63.w, - height: 30.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(30.w)), - gradient: const LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Color.fromRGBO(61, 138, 224, 1), - Color.fromRGBO(131, 89, 255, 1), - ], - ), - ), - child: Center( - child: Text( - "赠送", - style: TextStyle(fontSize: 13.w, color: Colors.white), + child: Center( + child: Text( + "赠送", + style: TextStyle(fontSize: 13.w, color: Colors.white), + ), ), ), ), @@ -425,40 +397,4 @@ class _LiveGiftPopupState extends State { ), ); } - - Widget _buildAdjustButton({ - required String label, - required bool enabled, - required VoidCallback onTap, - }) { - return InkWell( - onTap: enabled ? onTap : null, - child: Container( - width: 14.w, - height: 14.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(14.w)), - color: enabled - ? const Color.fromRGBO(117, 98, 249, 1) - : Colors.transparent, - border: Border.all( - width: 1, - color: const Color.fromRGBO(117, 98, 249, 1), - ), - ), - child: Center( - child: Text( - label, - style: TextStyle( - fontSize: 13.w, - color: enabled - ? Colors.white - : const Color.fromRGBO(117, 98, 249, 1), - height: 1, - ), - ), - ), - ), - ); - } } diff --git a/lib/widget/live/live_room_anchor_showcase.dart b/lib/widget/live/live_room_anchor_showcase.dart index 00acd48..68f74aa 100644 --- a/lib/widget/live/live_room_anchor_showcase.dart +++ b/lib/widget/live/live_room_anchor_showcase.dart @@ -158,7 +158,6 @@ class _LiveRoomAnchorShowcaseState extends State { } Widget _buildWaitingPlaceholder() { - Get.log("buildWaitingPlaceholder"); return Container( width: 177.w, height: 175.w, @@ -185,11 +184,10 @@ class _LiveRoomAnchorShowcaseState extends State { }) { final engine = _rtcManager.engine; final joined = _rtcManager.channelJoinedNotifier.value; - // 判断是否是当前用户 final bool isCurrentUser = - _roomController.currentRole == CurrentRole.maleAudience || - _roomController.currentRole == CurrentRole.femaleAudience; + _roomController.currentRole == CurrentRole.maleAudience && isLeft || + _roomController.currentRole == CurrentRole.femaleAudience && !isLeft; return Stack( children: [ ClipRRect( diff --git a/lib/widget/live/svga_player_widget.dart b/lib/widget/live/svga_player_widget.dart new file mode 100644 index 0000000..7d71124 --- /dev/null +++ b/lib/widget/live/svga_player_widget.dart @@ -0,0 +1,227 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svga/flutter_svga.dart'; +import 'package:get/get.dart'; +import 'package:dating_touchme_app/controller/discover/svga_player_manager.dart'; + +/// SVGA 动画播放 Widget +/// 监听 SvgaPlayerManager 的队列,自动播放 SVGA 动画 +class SvgaPlayerWidget extends StatefulWidget { + const SvgaPlayerWidget({super.key}); + + @override + State createState() => _SvgaPlayerWidgetState(); +} + +class _SvgaPlayerWidgetState extends State + with TickerProviderStateMixin { + final SvgaPlayerManager _manager = SvgaPlayerManager.instance; + SVGAAnimationController? _controller; + SvgaAnimationItem? _currentItem; + + @override + void initState() { + super.initState(); + // 监听队列变化 + ever(_manager.currentItem, (item) { + print( + '📢 currentItem 变化: ${item?.svgaFile ?? "null"}, 当前 _currentItem: ${_currentItem?.svgaFile ?? "null"}', + ); + if (item != null) { + // 如果当前没有播放,或者新的 item 与当前不同,则播放 + if (_currentItem == null || item.svgaFile != _currentItem!.svgaFile) { + print('🎯 准备播放新动画: ${item.svgaFile}'); + _playAnimation(item); + } else { + print('⚠️ 相同的动画,跳过播放'); + } + } else { + // currentItem 变为 null,但不立即清理,等待播放完成回调 + print('📢 currentItem 变为 null,等待播放完成回调'); + } + }); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + /// 播放动画 + Future _playAnimation(SvgaAnimationItem item) async { + print( + '🎬 开始播放动画: ${item.svgaFile}, 当前状态: _controller=${_controller != null}, _currentItem=${_currentItem?.svgaFile ?? "null"}', + ); + + // 如果正在播放,先停止并清理 + if (_controller != null) { + print('🛑 停止当前播放'); + _controller!.stop(); + _controller!.dispose(); + _controller = null; + } + + // 设置当前项并创建新的 controller + _currentItem = item; + _controller = SVGAAnimationController(vsync: this); + print('✅ 创建新的 controller,准备加载动画'); + + try { + // 判断是网络 URL 还是本地资源 + if (item.svgaFile.startsWith('http://') || + item.svgaFile.startsWith('https://')) { + // 网络 URL + SVGAParser.shared + .decodeFromURL(item.svgaFile) + .then((video) { + if (!mounted) return; + + // 检查是否还是当前要播放的动画 + if (_currentItem != item || _controller == null) { + print('⚠️ 动画已变更,取消播放: ${item.svgaFile}'); + return; + } + + _controller!.videoItem = video; + // 播放动画(repeat 会循环播放,我们需要监听完成) + _controller!.repeat(); + + // 获取动画时长,如果为 null 则使用默认值 3 秒 + final duration = _controller!.duration; + final playDuration = duration != null && duration > Duration.zero + ? duration + : const Duration(seconds: 3); + + print( + '✅ SVGA 动画加载成功(网络): ${item.svgaFile}, 播放时长: ${playDuration.inMilliseconds}ms', + ); + + // 在动画时长后停止并通知完成 + Future.delayed(playDuration, () { + if (!mounted) { + print('⚠️ Widget 已卸载,取消完成回调'); + return; + } + + // 再次检查是否还是当前动画 + if (_currentItem == item && _controller != null) { + print('✅ SVGA 动画播放完成(网络): ${item.svgaFile}'); + + // 先停止动画 + _controller!.stop(); + + // 清理当前项和 controller + final wasCurrentItem = _currentItem; + _currentItem = null; + _controller?.dispose(); + _controller = null; + + // 通知管理器播放完成(这会触发下一个动画) + if (wasCurrentItem == item) { + _manager.onAnimationFinished(); + } + } else { + print( + '⚠️ 动画已变更,跳过完成回调: _currentItem=${_currentItem?.svgaFile ?? "null"}, item=${item.svgaFile}', + ); + } + }); + }) + .catchError((error) { + print('❌ SVGA 动画加载失败(网络): $error'); + _currentItem = null; + _controller?.dispose(); + _controller = null; + _manager.onAnimationError(error.toString()); + }); + } else { + // 本地资源(assets) + SVGAParser.shared + .decodeFromAssets(item.svgaFile) + .then((video) { + if (!mounted) return; + + // 检查是否还是当前要播放的动画 + if (_currentItem != item || _controller == null) { + print('⚠️ 动画已变更,取消播放: ${item.svgaFile}'); + return; + } + + _controller!.videoItem = video; + // 播放动画(repeat 会循环播放,我们需要监听完成) + _controller!.repeat(); + + // 获取动画时长,如果为 null 则使用默认值 3 秒 + final duration = _controller!.duration; + final playDuration = duration != null && duration > Duration.zero + ? duration + : const Duration(seconds: 3); + + print( + '✅ SVGA 动画加载成功(本地): ${item.svgaFile}, 播放时长: ${playDuration.inMilliseconds}ms', + ); + + // 在动画时长后停止并通知完成 + Future.delayed(playDuration, () { + if (!mounted) { + print('⚠️ Widget 已卸载,取消完成回调'); + return; + } + + // 再次检查是否还是当前动画 + if (_currentItem == item && _controller != null) { + print('✅ SVGA 动画播放完成(本地): ${item.svgaFile}'); + + // 先停止动画 + _controller!.stop(); + + // 清理当前项和 controller + final wasCurrentItem = _currentItem; + _currentItem = null; + _controller?.dispose(); + _controller = null; + + // 通知管理器播放完成(这会触发下一个动画) + if (wasCurrentItem == item) { + _manager.onAnimationFinished(); + } + } else { + print( + '⚠️ 动画已变更,跳过完成回调: _currentItem=${_currentItem?.svgaFile ?? "null"}, item=${item.svgaFile}', + ); + } + }); + }) + .catchError((error) { + print('❌ SVGA 动画加载失败(本地): $error'); + _currentItem = null; + _controller?.dispose(); + _controller = null; + _manager.onAnimationError(error.toString()); + }); + } + } catch (e) { + print('❌ SVGA 播放异常: $e'); + _currentItem = null; + _controller?.dispose(); + _controller = null; + _manager.onAnimationError(e.toString()); + } + } + + @override + Widget build(BuildContext context) { + return Obx(() { + final currentItem = _manager.currentItem.value; + final isPlaying = _manager.isPlaying.value; + + if (!isPlaying || currentItem == null || _controller == null) { + return const SizedBox.shrink(); + } + + return Positioned.fill( + child: IgnorePointer(child: SVGAImage(_controller!)), + ); + }); + } +}