Browse Source

Merge branch 'master' of http://git.qniao.cn/dating-agency/dating_touchme_app

* 'master' of http://git.qniao.cn/dating-agency/dating_touchme_app:
  feat(live): 实现实名认证匹配功能并优化直播间动效播放
  添加用户信息页面跳转到聊天页面
  no message
  feat(discover): 实现SVGA动画播放管理器和播放组件
  feat(live): 实现礼物弹窗用户选择与礼物展示功能
  增加红娘等级,增加昵称最大宽度
  修改信息上传头像增加示例,增加跳转支付宝逻辑
  修改信息上传头像增加示例,增加跳转支付宝逻辑

# Conflicts:
#	lib/controller/discover/room_controller.dart
ios
ZHR007 4 months ago
parent
commit
24540fe479
15 changed files with 602 additions and 197 deletions
  1. 9
      ios/Podfile
  2. 103
      lib/controller/discover/room_controller.dart
  3. 86
      lib/controller/discover/svga_player_manager.dart
  4. 1
      lib/controller/mine/mine_controller.dart
  5. 1
      lib/controller/mine/rose_controller.dart
  6. 28
      lib/model/home/marriage_data.dart
  7. 2
      lib/network/api_urls.dart
  8. 4
      lib/network/rtc_api.dart
  9. 31
      lib/network/rtc_api.g.dart
  10. 4
      lib/pages/discover/live_room_page.dart
  11. 14
      lib/pages/home/user_information_page.dart
  12. 19
      lib/pages/mine/mine_page.dart
  13. 264
      lib/widget/live/live_gift_popup.dart
  14. 6
      lib/widget/live/live_room_anchor_showcase.dart
  15. 227
      lib/widget/live/svga_player_widget.dart

9
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

103
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<void> 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<void> 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<void> receiveRTCMessage(Map<String, dynamic> 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,

86
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<SvgaAnimationItem> _animationQueue = Queue<SvgaAnimationItem>();
final Rx<SvgaAnimationItem?> currentItem = Rx<SvgaAnimationItem?>(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;
}

1
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<Map> settingList = [

1
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"
));
}

28
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<dynamic>?)?.map((e) => PhotoItem.fromJson(e as Map<String, dynamic>)).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(),
);
}
}
//

2
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 =

4
lib/network/rtc_api.dart

@ -59,6 +59,10 @@ abstract class RtcApi {
@Body() Map<String, dynamic> data,
);
/// RTC
@POST(ApiUrls.destroyRtcChannel)
Future<HttpResponse<BaseResponse<dynamic>>> destroyRtcChannel();
/// RTC
@GET(ApiUrls.getRtcChannelPage)
Future<HttpResponse<BaseResponse<PaginatedResponse<RtcChannelModel>>>> getRtcChannelPage();

31
lib/network/rtc_api.g.dart

@ -287,6 +287,37 @@ class _RtcApi implements RtcApi {
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<dynamic>>> destroyRtcChannel() async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
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<Map<String, dynamic>>(_options);
late BaseResponse<dynamic> _value;
try {
_value = BaseResponse<dynamic>.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<HttpResponse<BaseResponse<PaginatedResponse<RtcChannelModel>>>>
getRtcChannelPage() async {

4
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<LiveRoomPage> {
],
),
),
// SVGA
const SvgaPlayerWidget(),
],
),
),

14
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),

19
lib/pages/mine/mine_page.dart

@ -99,12 +99,19 @@ class _MinePageState extends State<MinePage> 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,),

264
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<LiveGiftPopup> {
// ID集合
final Set<String> _selectedUserIds = <String>{};
// 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<RtcSeatUserInfo> users) {
setState(() {
if (_selectedUserIds.length == users.length) {
//
_selectedUserIds.clear();
} else {
//
_selectedUserIds.clear();
for (var user in users) {
_selectedUserIds.add(user.userId);
}
}
});
//
Future<void> _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<RoomController>()
? Get.find<RoomController>()
: 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<LiveGiftPopup> {
...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<LiveGiftPopup> {
}),
],
),
//
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<LiveGiftPopup> {
);
}
// 824
final itemsPerPage = 8;
final totalPages = (widget.giftList.length / itemsPerPage).ceil();
return Expanded(
child: ValueListenableBuilder<int?>(
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<LiveGiftPopup> {
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<LiveGiftPopup> {
),
);
}
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,
),
),
),
),
);
}
}

6
lib/widget/live/live_room_anchor_showcase.dart

@ -158,7 +158,6 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
}
Widget _buildWaitingPlaceholder() {
Get.log("buildWaitingPlaceholder");
return Container(
width: 177.w,
height: 175.w,
@ -185,11 +184,10 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
}) {
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(

227
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<SvgaPlayerWidget> createState() => _SvgaPlayerWidgetState();
}
class _SvgaPlayerWidgetState extends State<SvgaPlayerWidget>
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<void> _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!)),
);
});
}
}
Loading…
Cancel
Save