王子贤 4 months ago
parent
commit
3f060862f8
26 changed files with 967 additions and 560 deletions
  1. BIN
      assets/images/example_content.png
  2. BIN
      assets/images/room_video.png
  3. 244
      lib/controller/discover/room_controller.dart
  4. 1
      lib/generated/assets.dart
  5. 111
      lib/model/rtc/rtc_channel_detail.dart
  6. 2
      lib/network/api_service.g.dart
  7. 97
      lib/network/api_urls.dart
  8. 2
      lib/network/home_api.g.dart
  9. 35
      lib/network/rtc_api.dart
  10. 175
      lib/network/rtc_api.g.dart
  11. 2
      lib/network/user_api.g.dart
  12. 3
      lib/pages/discover/discover_page.dart
  13. 36
      lib/pages/discover/live_room_page.dart
  14. 2
      lib/pages/home/nearby_tab.dart
  15. 51
      lib/pages/home/recommend_tab.dart
  16. 67
      lib/pages/main/main_page.dart
  17. 182
      lib/pages/main/tabbar/main_tab_bar.dart
  18. 77
      lib/pages/main/tabbar/main_tab_btn.dart
  19. 13
      lib/pages/main/tabbar/main_tab_item.dart
  20. 50
      lib/rtc/rtc_manager.dart
  21. 16
      lib/rtc/rtm_manager.dart
  22. 9
      lib/service/live_chat_message_service.dart
  23. 165
      lib/widget/live/live_room_anchor_showcase.dart
  24. 69
      lib/widget/live/live_room_notice_chat_panel.dart
  25. 50
      lib/widget/live/live_room_user_header.dart
  26. 68
      location_plugin/example/pubspec.lock

BIN
assets/images/example_content.png

Before After
Width: 330  |  Height: 330  |  Size: 214 KiB

BIN
assets/images/room_video.png

Before After
Width: 72  |  Height: 54  |  Size: 610 B

244
lib/controller/discover/room_controller.dart

@ -1,7 +1,7 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:agora_token_generator/agora_token_generator.dart';
import 'package:dating_touchme_app/model/live/live_chat_message.dart';
import 'package:dating_touchme_app/controller/global.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart';
import 'package:dating_touchme_app/network/network_service.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
import 'package:dating_touchme_app/service/live_chat_message_service.dart';
@ -9,21 +9,36 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../model/live/live_chat_message.dart';
//
enum CurrentRole {
broadcaster, //
maleAudience, //
femaleAudience, //
audience, //
normalUser, //
}
///
class RoomController extends GetxController {
RoomController({NetworkService? networkService})
: _networkService = networkService ?? Get.find<NetworkService>();
: _networkService = networkService ?? Get.find<NetworkService>();
final NetworkService _networkService;
CurrentRole currentRole = CurrentRole.normalUser;
bool isLive = false;
///
final Rxn<RtcChannelData> rtcChannel = Rxn<RtcChannelData>();
final Rxn<RtcChannelDetail> rtcChannelDetail = Rxn<RtcChannelDetail>();
///
final RxList<LiveChatMessage> chatMessages = <LiveChatMessage>[].obs;
///
final LiveChatMessageService _messageService = LiveChatMessageService.instance;
final LiveChatMessageService _messageService =
LiveChatMessageService.instance;
@override
void onInit() {
@ -54,10 +69,12 @@ class RoomController extends GetxController {
///
void _addMessage(LiveChatMessage message) {
// userId + content + timestamp
final exists = chatMessages.any((m) =>
m.userId == message.userId &&
m.content == message.content &&
(m.timestamp - message.timestamp).abs() < 1000); // 1
final exists = chatMessages.any(
(m) =>
m.userId == message.userId &&
m.content == message.content &&
(m.timestamp - message.timestamp).abs() < 1000,
); // 1
if (exists) {
print('⚠️ 消息已存在,跳过添加');
@ -68,7 +85,7 @@ class RoomController extends GetxController {
print('✅ 消息已添加到列表,当前消息数: ${chatMessages.length}');
// 100
if (chatMessages.length > 100) {
if (chatMessages.length > 300) {
chatMessages.removeAt(0);
print('📝 消息列表已满,移除最旧的消息');
}
@ -84,7 +101,14 @@ class RoomController extends GetxController {
final base = response.data;
if (base.isSuccess && base.data != null) {
rtcChannel.value = base.data;
await _joinRtcChannel(base.data!.token, base.data!.channelId, base.data!.uid, ClientRoleType.clientRoleBroadcaster);
currentRole = CurrentRole.broadcaster;
isLive = true;
await _joinRtcChannel(
base.data!.token,
base.data!.channelId,
base.data!.uid,
ClientRoleType.clientRoleBroadcaster,
);
} else {
final message = base.message.isNotEmpty ? base.message : '创建频道失败';
SmartDialog.showToast(message);
@ -100,7 +124,13 @@ class RoomController extends GetxController {
final base = response.data;
if (base.isSuccess && base.data != null) {
rtcChannel.value = base.data;
await _joinRtcChannel(base.data!.token, channelName, base.data!.uid, ClientRoleType.clientRoleAudience);
currentRole = CurrentRole.normalUser;
await _joinRtcChannel(
base.data!.token,
channelName,
base.data!.uid,
ClientRoleType.clientRoleAudience,
);
}
} catch (e) {
SmartDialog.showToast('加入频道异常:$e');
@ -111,9 +141,10 @@ class RoomController extends GetxController {
String token,
String channelName,
int uid,
ClientRoleType roleType
ClientRoleType roleType,
) async {
try {
await _fetchRtcChannelDetail(channelName);
await RTCManager.instance.joinChannel(
token: token,
channelId: channelName,
@ -125,9 +156,94 @@ class RoomController extends GetxController {
}
}
Future<void> joinChat(CurrentRole role) async {
final data = {
'channelId': RTCManager.instance.currentChannelId,
'seatNumber': role == CurrentRole.maleAudience ? 1 : 2,
'isMicrophoneOn': role != CurrentRole.normalUser ? true : false,
'isVideoOn':
role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience
? true
: false,
};
final response = await _networkService.rtcApi.connectRtcChannel(data);
if (!response.data.isSuccess) {
SmartDialog.showToast(response.data.message);
return;
}
currentRole = role;
if (role == CurrentRole.maleAudience ||
role == CurrentRole.femaleAudience) {
await RTCManager.instance.publishVideo(role);
} else {
await RTCManager.instance.publishAudio();
}
RtcSeatUserInfo userInfo = RtcSeatUserInfo(
uid: rtcChannel.value?.uid,
miId: GlobalData().userData?.id ?? '',
userId: GlobalData().userData?.id ?? '',
nickName: GlobalData().userData?.nickName ?? '',
profilePhoto: GlobalData().userData?.profilePhoto ?? '',
seatNumber: role == CurrentRole.maleAudience ? 1 : 2,
isFriend: false,
isMicrophoneOn: true,
isVideoOn: role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience ? true : false,
genderCode: GlobalData().userData?.genderCode ?? 0,
);
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
anchorInfo: rtcChannelDetail.value!.anchorInfo,
maleInfo: role == CurrentRole.maleAudience ? userInfo : null,
femaleInfo: role == CurrentRole.femaleAudience ? userInfo : null,
);
rtcChannelDetail.value = newDetail;
isLive = true;
}
Future<void> leaveChat() async {
final data = {'channelId': RTCManager.instance.currentChannelId};
final response = await _networkService.rtcApi.disconnectRtcChannel(data);
if (response.data.isSuccess) {
isLive = false;
await RTCManager.instance.unpublish(currentRole);
if (currentRole == CurrentRole.maleAudience) {
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
anchorInfo: rtcChannelDetail.value!.anchorInfo,
maleInfo: null,
femaleInfo: rtcChannelDetail.value!.femaleInfo,
);
rtcChannelDetail.value = newDetail;
} else if (currentRole == CurrentRole.femaleAudience) {
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
anchorInfo: rtcChannelDetail.value!.anchorInfo,
maleInfo: rtcChannelDetail.value!.maleInfo,
femaleInfo: null,
);
rtcChannelDetail.value = newDetail;
}
currentRole = CurrentRole.normalUser;
}
}
Future<void> _fetchRtcChannelDetail(String channelName) async {
try {
final response = await _networkService.rtcApi.getRtcChannelDetail(
channelName,
);
final base = response.data;
if (base.isSuccess && base.data != null) {
rtcChannelDetail.value = base.data;
}
} catch (e) {
print('获取 RTC 频道详情失败:$e');
}
}
///
Future<void> sendChatMessage(String content) async {
final channelName = rtcChannel.value?.channelId ?? RTCManager.instance.currentChannelId;
final channelName = RTCManager.instance.currentChannelId;
final result = await _messageService.sendMessage(
content: content,
@ -140,11 +256,6 @@ class RoomController extends GetxController {
}
}
///
Future<void> sendMessage(String message) async {
await sendChatMessage(message);
}
Future<bool> _ensureRtcPermissions() async {
final statuses = await [Permission.camera, Permission.microphone].request();
final allGranted = statuses.values.every((status) => status.isGranted);
@ -152,8 +263,9 @@ class RoomController extends GetxController {
return true;
}
final permanentlyDenied =
statuses.values.any((status) => status.isPermanentlyDenied);
final permanentlyDenied = statuses.values.any(
(status) => status.isPermanentlyDenied,
);
if (permanentlyDenied) {
SmartDialog.showToast('请在系统设置中开启摄像头和麦克风权限');
await openAppSettings();
@ -164,7 +276,97 @@ class RoomController extends GetxController {
}
Future<void> leaveChannel() async {
isLive = false;
if (currentRole == CurrentRole.maleAudience || currentRole == CurrentRole.femaleAudience) {
await RTCManager.instance.unpublish(currentRole);
}
currentRole = CurrentRole.normalUser;;
await RTCManager.instance.leaveChannel();
}
}
/// RTC消息
Future<void> receiveRTCMessage(Map<String, dynamic> message) async {
if (message['type'] == 'join_chat') {
final response = await _networkService.rtcApi.getDatingRtcChannelUserDetail(
rtcChannel.value!.channelId,
message['uid'],
);
if (!response.data.isSuccess) {
return;
}
final currentDetail = rtcChannelDetail.value;
if (currentDetail == null) {
return;
}
if (message['role'] == 'male_audience') {
final userData = response.data.data;
// if (userData != null) {
final maleInfo = RtcSeatUserInfo(
miId: rtcChannelDetail.value!.anchorInfo!.miId,
userId: rtcChannelDetail.value!.anchorInfo!.userId,
nickName: rtcChannelDetail.value!.anchorInfo!.nickName,
profilePhoto: rtcChannelDetail.value!.anchorInfo!.profilePhoto,
genderCode: rtcChannelDetail.value!.anchorInfo!.genderCode,
seatNumber: rtcChannelDetail.value!.anchorInfo!.seatNumber,
isFriend: rtcChannelDetail.value!.anchorInfo!.isFriend,
isMicrophoneOn: rtcChannelDetail.value!.anchorInfo!.isMicrophoneOn,
isVideoOn: rtcChannelDetail.value!.anchorInfo!.isVideoOn,
uid: message['uid'] is int
? message['uid'] as int
: int.tryParse(message['uid']?.toString() ?? ''),
);
final newDetail = RtcChannelDetail(
channelId: currentDetail.channelId,
anchorInfo: currentDetail.anchorInfo,
maleInfo: maleInfo,
femaleInfo: currentDetail.femaleInfo,
);
rtcChannelDetail.value = newDetail;
// }
} else if (message['role'] == 'female_audience') {
final userData = response.data.data;
// if (userData != null) {
final femaleInfo = RtcSeatUserInfo(
miId: rtcChannelDetail.value!.anchorInfo!.miId,
userId: rtcChannelDetail.value!.anchorInfo!.userId,
nickName: rtcChannelDetail.value!.anchorInfo!.nickName,
profilePhoto: rtcChannelDetail.value!.anchorInfo!.profilePhoto,
genderCode: rtcChannelDetail.value!.anchorInfo!.genderCode,
seatNumber: rtcChannelDetail.value!.anchorInfo!.seatNumber,
isFriend: rtcChannelDetail.value!.anchorInfo!.isFriend,
isMicrophoneOn: rtcChannelDetail.value!.anchorInfo!.isMicrophoneOn,
isVideoOn: rtcChannelDetail.value!.anchorInfo!.isVideoOn,
uid: message['uid'] is int
? message['uid'] as int
: int.tryParse(message['uid']?.toString() ?? ''),
);
final newDetail = RtcChannelDetail(
channelId: currentDetail.channelId,
anchorInfo: currentDetail.anchorInfo,
maleInfo: currentDetail.maleInfo,
femaleInfo: femaleInfo,
);
rtcChannelDetail.value = newDetail;
}
// }
}else if (message['type'] == 'leave_chat') {
if (message['role'] == 'male_audience') {
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
anchorInfo: rtcChannelDetail.value!.anchorInfo,
maleInfo: null,
femaleInfo: rtcChannelDetail.value!.femaleInfo,
);
rtcChannelDetail.value = newDetail;
} else if (message['role'] == 'female_audience') {
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
anchorInfo: rtcChannelDetail.value!.anchorInfo,
maleInfo: rtcChannelDetail.value!.maleInfo,
femaleInfo: null,
);
rtcChannelDetail.value = newDetail;
}
}
}
}

1
lib/generated/assets.dart

@ -150,6 +150,7 @@ class Assets {
static const String imagesRocket1 = 'assets/images/rocket1.svga';
static const String imagesRocket2 = 'assets/images/rocket2.svga';
static const String imagesRocket3 = 'assets/images/rocket3.svga';
static const String imagesRoomVideo = 'assets/images/room_video.png';
static const String imagesRose = 'assets/images/rose.png';
static const String imagesRoseBanner = 'assets/images/rose_banner.png';
static const String imagesRoseGift = 'assets/images/rose_gift.png';

111
lib/model/rtc/rtc_channel_detail.dart

@ -0,0 +1,111 @@
class RtcChannelDetail {
final String channelId;
final RtcSeatUserInfo? anchorInfo;
final RtcSeatUserInfo? maleInfo;
final RtcSeatUserInfo? femaleInfo;
const RtcChannelDetail({
required this.channelId,
this.anchorInfo,
this.maleInfo,
this.femaleInfo,
});
factory RtcChannelDetail.fromJson(Map<String, dynamic> json) {
return RtcChannelDetail(
channelId: json['channelId']?.toString() ?? '',
anchorInfo: _parseSeatInfo(json['anchorInfo']),
maleInfo: _parseSeatInfo(json['maleInfo']),
femaleInfo: _parseSeatInfo(json['femaleInfo']),
);
}
Map<String, dynamic> toJson() {
return {
'channelId': channelId,
'anchorInfo': anchorInfo?.toJson(),
'maleInfo': maleInfo?.toJson(),
'femaleInfo': femaleInfo?.toJson(),
};
}
static RtcSeatUserInfo? _parseSeatInfo(dynamic value) {
if (value is Map<String, dynamic>) {
return RtcSeatUserInfo.fromJson(value);
}
return null;
}
}
class RtcSeatUserInfo {
final String miId;
final String userId;
final String nickName;
final String profilePhoto;
final int genderCode;
final int seatNumber;
final bool isFriend;
final bool isMicrophoneOn;
final bool isVideoOn;
final int? uid;
const RtcSeatUserInfo({
required this.miId,
required this.userId,
required this.nickName,
required this.profilePhoto,
required this.genderCode,
required this.seatNumber,
required this.isFriend,
required this.isMicrophoneOn,
required this.isVideoOn,
this.uid,
});
factory RtcSeatUserInfo.fromJson(Map<String, dynamic> json) {
return RtcSeatUserInfo(
miId: json['miId']?.toString() ?? '',
userId: json['userId']?.toString() ?? '',
nickName: json['nickName']?.toString() ?? '',
profilePhoto: json['profilePhoto']?.toString() ?? '',
genderCode: json['genderCode'] is int
? json['genderCode'] as int
: int.tryParse(json['genderCode']?.toString() ?? '0') ?? 0,
seatNumber: json['seatNumber'] is int
? json['seatNumber'] as int
: int.tryParse(json['seatNumber']?.toString() ?? '0') ?? 0,
isFriend: _parseBool(json['isFriend']),
isMicrophoneOn: _parseBool(json['isMicrophoneOn']),
isVideoOn: _parseBool(json['isVideoOn']),
uid: json['uid'] is int
? json['uid'] as int
: int.tryParse(json['uid']?.toString() ?? ''),
);
}
Map<String, dynamic> toJson() {
return {
'miId': miId,
'userId': userId,
'nickName': nickName,
'profilePhoto': profilePhoto,
'genderCode': genderCode,
'seatNumber': seatNumber,
'isFriend': isFriend,
'isMicrophoneOn': isMicrophoneOn,
'isVideoOn': isVideoOn,
'uid': uid,
};
}
static bool _parseBool(dynamic value) {
if (value is bool) return value;
if (value is num) return value != 0;
if (value is String) {
return value == '1' ||
value.toLowerCase() == 'true' ||
value.toLowerCase() == 'yes';
}
return false;
}
}

2
lib/network/api_service.g.dart

@ -8,7 +8,7 @@ part of 'api_service.dart';
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter,avoid_unused_constructor_parameters,unreachable_from_main
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter
class _ApiService implements ApiService {
_ApiService(this._dio, {this.baseUrl, this.errorLogger}) {

97
lib/network/api_urls.dart

@ -3,40 +3,77 @@
class ApiUrls {
//
static const String login = 'dating-agency-uec/authorize/by-captcha';
static const String getVerificationCode = 'dating-agency-uec/authorize/get/auth-captcha';
static const String getVerificationCode =
'dating-agency-uec/authorize/get/auth-captcha';
//
static const String getBaseUserInfo = 'dating-agency-uec/user/get/base-info';
static const String updatePhone = 'dating-agency-uec/user/modify/phone';
static const String getMarriageInformationDetail = 'dating-agency-service/user/get/dongwo/marriage-information-detail';
static const String registerMarriageInformation = 'dating-agency-service/user/register/marriage-information';
static const String getHxUserToken = 'dating-agency-chat-audio/user/get/hx/user/token';
static const String getApplyTempAuth = 'dating-agency-uec/get/apply-temp-auth';
static const String saveCertificationAudit = 'dating-agency-service/user/save/certification/audit';
static const String listVirtualCurrencyProduct = 'dating-agency-mall/user/list/virtual-currency-product';
static const String getVirtualAccount = 'dating-agency-mall/user/get/virtual-account';
static const String pageVirtualAccountRecord = 'dating-agency-mall/user/page/virtual-account-record';
static const String getCertificationList = 'dating-agency-service/user/get/certification/item/all/list';
static const String getMarriageInformationDetail =
'dating-agency-service/user/get/dongwo/marriage-information-detail';
static const String registerMarriageInformation =
'dating-agency-service/user/register/marriage-information';
static const String getHxUserToken =
'dating-agency-chat-audio/user/get/hx/user/token';
static const String getApplyTempAuth =
'dating-agency-uec/get/apply-temp-auth';
static const String saveCertificationAudit =
'dating-agency-service/user/save/certification/audit';
static const String listVirtualCurrencyProduct =
'dating-agency-mall/user/list/virtual-currency-product';
static const String getVirtualAccount =
'dating-agency-mall/user/get/virtual-account';
static const String pageVirtualAccountRecord =
'dating-agency-mall/user/page/virtual-account-record';
static const String getCertificationList =
'dating-agency-service/user/get/certification/item/all/list';
static const String submitOrder = 'dating-agency-mall/user/submit/order';
static const String getEducationList = 'dating-agency-service/user/get/education/list';
static const String getIncomeList = 'dating-agency-service/user/get/income/list';
static const String getMaritalStatusList = 'dating-agency-service/user/get/marital/status/list';
static const String getPropertyList = 'dating-agency-service/user/get/property/permits';
static const String getOccupationList = 'dating-agency-service/user/get/occupation/list';
static const String getSwRtcToken = 'dating-agency-chat-audio/user/get/sw/rtc/token';
static const String createRtcChannel = 'dating-agency-chat-audio/user/create/rtc-channel';
static const String editOwnMarriageInformation = 'dating-agency-service/user/edit/own-marriage-information';
static const String getSwRtmToken = 'dating-agency-chat-audio/user/get/sw/rtm/token';
static const String listBankCardByIndividual = 'dating-agency-mall/user/list/bank-card/by-individual';
static const String createBankCardByIndividual = 'dating-agency-mall/user/create/bank-card/by-individual';
static const String recognizeBankCard = 'dating-agency-uec/user/recognize/bank-card';
static const String calculateWithdrawServiceFee = 'dating-agency-mall/user/calculate/withdraw-service-fee';
static const String applyWalletAccountWithdraw = 'dating-agency-mall/user/apply/wallet-account/withdraw';
static const String pageWithdrawAudit = 'dating-agency-mall/user/page/withdraw-audit';
static const String getMarriageInformationDetailsById = 'dating-agency-service/user/get/marriage/information/details/byid';
static const String getEducationList =
'dating-agency-service/user/get/education/list';
static const String getIncomeList =
'dating-agency-service/user/get/income/list';
static const String getMaritalStatusList =
'dating-agency-service/user/get/marital/status/list';
static const String getPropertyList =
'dating-agency-service/user/get/property/permits';
static const String getOccupationList =
'dating-agency-service/user/get/occupation/list';
static const String getSwRtcToken =
'dating-agency-chat-audio/user/get/sw/rtc/token';
static const String createRtcChannel =
'dating-agency-chat-audio/user/create/rtc-channel';
static const String editOwnMarriageInformation =
'dating-agency-service/user/edit/own-marriage-information';
static const String getSwRtmToken =
'dating-agency-chat-audio/user/get/sw/rtm/token';
static const String getRtcChannelDetail =
'dating-agency-chat-audio/user/get/dating-rtc-channel/detail';
static const String connectRtcChannel =
'dating-agency-chat-audio/user/connect/rtc-channel';
static const String getDatingRtcChannelUserDetail =
'dating-agency-chat-audio/user/get/dating-rtc-channel-user/detail';
static const String enableRtcChannelUserAudio =
'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 listBankCardByIndividual =
'dating-agency-mall/user/list/bank-card/by-individual';
static const String createBankCardByIndividual =
'dating-agency-mall/user/create/bank-card/by-individual';
static const String recognizeBankCard =
'dating-agency-uec/user/recognize/bank-card';
static const String calculateWithdrawServiceFee =
'dating-agency-mall/user/calculate/withdraw-service-fee';
static const String applyWalletAccountWithdraw =
'dating-agency-mall/user/apply/wallet-account/withdraw';
static const String pageWithdrawAudit =
'dating-agency-mall/user/page/withdraw-audit';
static const String getMarriageInformationDetailsById =
'dating-agency-service/user/get/marriage/information/details/byid';
//
static const String getMarriageList = 'dating-agency-service/user/page/dongwo/marriage-information';
static const String getMarriageList =
'dating-agency-service/user/page/dongwo/marriage-information';
// API端点
}
}

2
lib/network/home_api.g.dart

@ -8,7 +8,7 @@ part of 'home_api.dart';
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter,avoid_unused_constructor_parameters,unreachable_from_main
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter
class _HomeApi implements HomeApi {
_HomeApi(this._dio, {this.baseUrl, this.errorLogger});

35
lib/network/rtc_api.dart

@ -1,4 +1,5 @@
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart';
import 'package:dating_touchme_app/network/api_urls.dart';
import 'package:dating_touchme_app/network/response_model.dart';
import 'package:dio/dio.dart';
@ -14,7 +15,7 @@ abstract class RtcApi {
///
@GET(ApiUrls.getSwRtcToken)
Future<HttpResponse<BaseResponse<RtcChannelData>>> getSwRtcToken(
@Query('channelId') String channelId,
@Query('channelId') String channelId,
);
/// RTM Token
@ -24,5 +25,35 @@ abstract class RtcApi {
///
@POST(ApiUrls.createRtcChannel)
Future<HttpResponse<BaseResponse<RtcChannelData>>> createRtcChannel();
}
/// RTC
@GET(ApiUrls.getRtcChannelDetail)
Future<HttpResponse<BaseResponse<RtcChannelDetail>>> getRtcChannelDetail(
@Query('channelId') String channelId,
);
/// RTC
@POST(ApiUrls.connectRtcChannel)
Future<HttpResponse<BaseResponse<dynamic>>> connectRtcChannel(
@Body() Map<String, dynamic> data,
);
/// RTC
@GET(ApiUrls.getDatingRtcChannelUserDetail)
Future<HttpResponse<BaseResponse<RtcSeatUserInfo>>> getDatingRtcChannelUserDetail(
@Query('channelId') String channelId,
@Query('uId') int uId,
);
/// / RTC
@POST(ApiUrls.enableRtcChannelUserAudio)
Future<HttpResponse<BaseResponse<dynamic>>> enableRtcChannelUserAudio(
@Body() Map<String, dynamic> data,
);
/// RTC
@POST(ApiUrls.disconnectRtcChannel)
Future<HttpResponse<BaseResponse<dynamic>>> disconnectRtcChannel(
@Body() Map<String, dynamic> data,
);
}

175
lib/network/rtc_api.g.dart

@ -8,7 +8,7 @@ part of 'rtc_api.dart';
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter,avoid_unused_constructor_parameters,unreachable_from_main
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter
class _RtcApi implements RtcApi {
_RtcApi(this._dio, {this.baseUrl, this.errorLogger});
@ -114,6 +114,179 @@ class _RtcApi implements RtcApi {
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<RtcChannelDetail>>> getRtcChannelDetail(
String channelId,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{r'channelId': channelId};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options =
_setStreamType<HttpResponse<BaseResponse<RtcChannelDetail>>>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/get/dating-rtc-channel/detail',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl),
),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<RtcChannelDetail> _value;
try {
_value = BaseResponse<RtcChannelDetail>.fromJson(
_result.data!,
(json) => RtcChannelDetail.fromJson(json as Map<String, dynamic>),
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<dynamic>>> connectRtcChannel(
Map<String, dynamic> data,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(data);
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/connect/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<RtcSeatUserInfo>>>
getDatingRtcChannelUserDetail(String channelId, int uId) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{
r'channelId': channelId,
r'uId': uId,
};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<HttpResponse<BaseResponse<RtcSeatUserInfo>>>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/get/dating-rtc-channel-user/detail',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<RtcSeatUserInfo> _value;
try {
_value = BaseResponse<RtcSeatUserInfo>.fromJson(
_result.data!,
(json) => RtcSeatUserInfo.fromJson(json as Map<String, dynamic>),
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<dynamic>>> enableRtcChannelUserAudio(
Map<String, dynamic> data,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(data);
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/enable/rtc-channel-user/audio',
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<dynamic>>> disconnectRtcChannel(
Map<String, dynamic> data,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(data);
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/disconnect/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;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||

2
lib/network/user_api.g.dart

@ -8,7 +8,7 @@ part of 'user_api.dart';
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter,avoid_unused_constructor_parameters,unreachable_from_main
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter
class _UserApi implements UserApi {
_UserApi(this._dio, {this.baseUrl, this.errorLogger});

3
lib/pages/discover/discover_page.dart

@ -62,7 +62,6 @@ class _DiscoverPageState extends State<DiscoverPage>
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
height: MediaQuery.of(context).size.height - 64,
// constraints: BoxConstraints(minHeight: ScreenUtil().setHeight(800)),
child: Column(
children: [
@ -167,7 +166,7 @@ class _LiveItemState extends State<LiveItem> {
return InkWell(
onTap: () async{
// Get.to(() => LiveRoomPage(id: 0));
await roomController.joinChannel('1189028638616588288');
await roomController.joinChannel('1190140590348701696');
},
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10.w)),

36
lib/pages/discover/live_room_page.dart

@ -89,7 +89,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
//
await _roomController.sendChatMessage(content);
//
_messageController.clear();
message = '';
@ -159,29 +159,23 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
child: Column(
children: [
SizedBox(height: 10.w),
Builder(
builder: (context) {
// GlobalData
final userData = GlobalData().userData;
final userName = userData?.nickName ?? '用户';
//
final popularityText = '0'; // TODO:
return LiveRoomUserHeader(
userName: userName,
popularityText: popularityText,
avatarAsset: (userData?.profilePhoto != null &&
userData!.profilePhoto!.isNotEmpty)
? userData.profilePhoto!
: Assets.imagesUserAvatar,
);
},
),
Obx(() {
final detail = _roomController.rtcChannelDetail.value;
final anchorInfo = detail?.anchorInfo;
final userName = anchorInfo!.nickName;
final avatarAsset = anchorInfo.profilePhoto;
const popularityText = '0'; // TODO: 使
return LiveRoomUserHeader(
userName: userName,
popularityText: popularityText,
avatarAsset: avatarAsset,
);
}),
SizedBox(height: 7.w),
LiveRoomAnchorShowcase(),
SizedBox(height: 5.w),
const LiveRoomSeatList(),
SizedBox(height: 5.w),
const LiveRoomActiveSpeaker(),
SizedBox(height: 9.w),
const LiveRoomNoticeChatPanel(),

2
lib/pages/home/nearby_tab.dart

@ -94,7 +94,7 @@ class _NearbyTabState extends State<NearbyTab> with AutomaticKeepAliveClientMixi
height: MediaQuery.of(context).size.height - totalBottomPadding,
child: ListView.separated(
// padding AppBar
padding: EdgeInsets.only(left: 12, right: 12, bottom: 12),
padding: EdgeInsets.only(left: 12, right: 12),
itemBuilder: (context, index) {
//
if (controller.nearbyFeed.isEmpty && index == 0) {

51
lib/pages/home/recommend_tab.dart

@ -37,6 +37,18 @@ class _RecommendTabState extends State<RecommendTab>
final tabBarHeight = 64.0;
final totalBottomPadding = bottomPadding + tabBarHeight;
return Obx(() {
if (controller.recommendIsLoading.value && controller.recommendFeed.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('加载数据中...'),
],
),
);
}
return EasyRefresh(
controller: _refreshController,
header: const ClassicHeader(
@ -90,38 +102,32 @@ class _RecommendTabState extends State<RecommendTab>
_refreshController.finishLoad(IndicatorResult.fail);
}
},
child: SizedBox(
height: MediaQuery.of(context).size.height - totalBottomPadding,
child: ListView.separated(
child: ListView.separated(
// 使
// padding AppBar
padding: EdgeInsets.only(left: 12, right: 12, bottom: 12),
padding: EdgeInsets.only(left: 12, right: 12),
itemBuilder: (context, index) {
//
if (controller.recommendFeed.isEmpty && index == 0) {
// 使
if (controller.recommendIsLoading.value) {
return SizedBox(
height: MediaQuery.of(context).size.height - totalBottomPadding,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('加载数据中...'),
],
),
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('加载数据中...'),
],
),
);
} else {
return SizedBox(
height: MediaQuery.of(context).size.height - totalBottomPadding,
child: const Center(
child: Text(
"暂无数据",
style: TextStyle(fontSize: 14, color: Color(0xFF999999)),
),
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('暂无数据'),
],
),
);
}
@ -140,7 +146,6 @@ class _RecommendTabState extends State<RecommendTab>
// item
itemCount: controller.recommendFeed.isEmpty ? 1 : controller.recommendFeed.length,
)
),
);
});
}

67
lib/pages/main/main_page.dart

@ -1,5 +1,4 @@
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/pages/main/tabbar/main_tab_bar.dart';
import 'package:dating_touchme_app/pages/message/message_page.dart';
import 'package:dating_touchme_app/pages/mine/mine_page.dart';
import 'package:dating_touchme_app/rtc/rtm_manager.dart';
@ -10,6 +9,7 @@ import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:dating_touchme_app/controller/mine/user_controller.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import '../../extension/router_service.dart';
import '../../widget/double_tap_to_exit_widget.dart';
@ -67,18 +67,6 @@ class _MainPageState extends State<MainPage> {
initRTM() async {
String? userId = storage.read<String>('userId');
await RTMManager.instance.initialize(appId: '4c2ea9dcb4c5440593a418df0fdd512d', userId: userId ?? '');
SmartDialog.show(
alignment: Alignment.center,
builder: (context){
return SVGAEasyPlayer(
assetsName: Assets.imagesRocket2,
fit: BoxFit.contain,
);
}
);
Future.delayed(const Duration(seconds: 4), () {
SmartDialog.dismiss();
});
}
@override
@ -87,29 +75,44 @@ class _MainPageState extends State<MainPage> {
child: Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
body: Stack(
alignment: Alignment.bottomCenter,
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
children: [
PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
children: [
homePage, // 使
discoverPage,
messagePage,
minePage,
],
),
MainTabBar(
initialIndex: currentIndex,
onTabChanged: (index) {
currentIndex = index;
pageController.jumpToPage(index);
},
),
homePage, // 使
discoverPage,
messagePage,
minePage,
],
),
bottomNavigationBar: TDBottomTabBar(
currentIndex: currentIndex,
TDBottomTabBarBasicType.iconText,
componentType: TDBottomTabBarComponentType.normal,
useVerticalDivider: false,
navigationTabs: [
tabItem('首页', Assets.imagesHomePre, Assets.imagesHomeNol, 0),
tabItem('找对象', Assets.imagesDiscoverPre, Assets.imagesDiscoverNol, 1),
tabItem('消息', Assets.imagesMessagePre, Assets.imagesMessageNol, 2),
tabItem('我的', Assets.imagesMinePre, Assets.imagesMineNol, 3),
]
)
),
);
}
/// item
TDBottomTabBarTabConfig tabItem(String title, String selectedIcon, String unselectedIcon, int index) {
return TDBottomTabBarTabConfig(
tabText: title,
selectedIcon: Image.asset(selectedIcon, width: 25, height: 25, fit: BoxFit.cover),
unselectedIcon: Image.asset(unselectedIcon, width: 25, height: 25, fit: BoxFit.cover),
selectTabTextStyle: TextStyle(color: Color(0xFFED4AC3)),
unselectTabTextStyle: TextStyle(color: Color(0xFF999999)),
onTap: () {
currentIndex = index;
pageController.jumpToPage(index);
},
);
}
}

182
lib/pages/main/tabbar/main_tab_bar.dart

@ -1,182 +0,0 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dating_touchme_app/extension/ex_context.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../generated/assets.dart';
import 'main_tab_btn.dart';
import 'main_tab_item.dart';
class MainTabBar extends StatefulWidget {
const MainTabBar({super.key, this.initialIndex = 0, this.onTabChanged});
final int initialIndex;
final void Function(int index)? onTabChanged;
@override
State<MainTabBar> createState() => _MainTabBarState();
}
class _MainTabBarState extends State<MainTabBar> {
int _selecteIndex = 0;
final List<MainTabItem> items = const [
MainTabItem(
title: "首页",
icon: Assets.imagesHomeNol,
selectedIcon: Assets.imagesHomePre,
),
MainTabItem(
title: "找对象",
icon: Assets.imagesDiscoverNol,
selectedIcon: Assets.imagesDiscoverPre,
),
MainTabItem(
title: "消息",
icon: Assets.imagesMessageNol,
selectedIcon: Assets.imagesMessagePre,
),
MainTabItem(
title: "我的",
icon: Assets.imagesMineNol,
selectedIcon: Assets.imagesMinePre,
),
];
@override
void initState() {
_selecteIndex = widget.initialIndex;
super.initState();
}
@override
void didUpdateWidget(covariant MainTabBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (_selecteIndex != widget.initialIndex) {
_selecteIndex = widget.initialIndex;
}
}
@override
Widget build(BuildContext context) {
return Builder(builder: (context) {
if (Platform.isIOS) {
if (context.bottomPadding > 0) {
return Container(
height: 64.w,
color: Colors.white,
child: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: items.mapIndexed((index, item) {
return MainTabButton(
selected: index == _selecteIndex,
isRedDot: item.showRedDot,
title: item.title,
onTap: () {
if (_selecteIndex != index) {
_selecteIndex = index;
setState(() {});
widget.onTabChanged?.call(index);
}
},
icon: item.icon,
selectedIcon: item.selectedIcon,
);
}).toList()),
SizedBox(
height: 22.w,
)
],
)
],
),
);
} else {
return SafeArea(
top: false,
child: Container(
height: 64.w,
color: Colors.white,
child: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: items.mapIndexed((index, item) {
return MainTabButton(
selected: index == _selecteIndex,
isRedDot: item.showRedDot,
title: item.title,
onTap: () {
if (_selecteIndex != index) {
_selecteIndex = index;
setState(() {});
widget.onTabChanged?.call(index);
}
},
icon: item.icon,
selectedIcon: item.selectedIcon,
);
}).toList()),
SizedBox(
height: 14.w,
)
],
)
],
),
),
);
}
} else {
return SafeArea(
top: false,
child: Container(
// padding: EdgeInsets.only(bottom: 14.w),
// margin: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 14.w),
height: 64.w,
color: Colors.white,
child: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: items.mapIndexed((index, item) {
return MainTabButton(
selected: index == _selecteIndex,
isRedDot: item.showRedDot,
title: item.title,
onTap: () {
if (_selecteIndex != index) {
_selecteIndex = index;
setState(() {});
widget.onTabChanged?.call(index);
}
},
icon: item.icon,
selectedIcon: item.selectedIcon,
);
}).toList()),
SizedBox(
height: 14.w,
)
],
)
],
),
),
);
}
});
}
}

77
lib/pages/main/tabbar/main_tab_btn.dart

@ -1,77 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class MainTabButton extends StatefulWidget {
final VoidCallback? onTap;
final bool isRedDot;
final bool selected;
final String icon;
final String title;
final String selectedIcon;
const MainTabButton({
super.key,
required this.selected,
required this.icon,
required this.selectedIcon,
this.isRedDot = false,
this.onTap,
required this.title
});
@override
State<MainTabButton> createState() => MainTabButtonState();
}
class MainTabButtonState extends State<MainTabButton> {
@override
Widget build(BuildContext context) {
if (widget.isRedDot) {
return Expanded(
child: GestureDetector(
onTap: widget.onTap,
child: Container(
color: Colors.transparent,
alignment: Alignment.center,
child: Stack(
clipBehavior: Clip.none,
children: [
Column(
children: [
SizedBox(height: 5.w,),
widget.selected
? Image.asset(widget.selectedIcon, width: 25.w, height: 25.w)
: Image.asset(widget.icon, width: 25.w, height: 25.w),
Text(widget.title, style: const TextStyle(
fontSize: 12
),)
],
),
],
),
),
));
}
return Expanded(
child: GestureDetector(
onTap: widget.onTap,
child: Container(
alignment: Alignment.center,
color: Colors.transparent,
child: Column(
children: [
SizedBox(height: 5.w,),
widget.selected
? Image.asset(widget.selectedIcon, width: 25.w, height: 25.w)
: Image.asset(widget.icon, width: 25.w, height: 25.w),
Text(widget.title, style: const TextStyle(
fontSize: 12
),)
],
),
),
),
);
}
}

13
lib/pages/main/tabbar/main_tab_item.dart

@ -1,13 +0,0 @@
class MainTabItem {
final String title;
final String icon;
final String selectedIcon;
final bool showRedDot;
const MainTabItem({
required this.title,
required this.icon,
required this.selectedIcon,
this.showRedDot = false,
});
}

50
lib/rtc/rtc_manager.dart

@ -5,6 +5,8 @@ import 'package:dating_touchme_app/rtc/rtm_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import '../controller/discover/room_controller.dart';
import '../network/network_service.dart';
import '../pages/discover/live_room_page.dart';
/// RTC
@ -14,6 +16,7 @@ class RTCManager {
final ValueNotifier<List<int>> remoteUsersNotifier = ValueNotifier<List<int>>(
<int>[],
);
NetworkService get _networkService => NetworkService();
RtcEngine? get engine => _engine;
bool get isInChannel => _isInChannel;
int? get currentUid => _currentUid;
@ -118,7 +121,6 @@ class RTCManager {
_currentChannelId = connection.channelId;
print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms');
if (connection.localUid == _currentUid) {
await RTMManager.instance.subscribe(_currentChannelId ?? '');
await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '',
@ -130,7 +132,7 @@ class RTCManager {
onJoinChannelSuccess!(connection, elapsed);
}
},
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) async{
print('用户加入,UID:$remoteUid');
_handleRemoteUserJoined(remoteUid);
if (onUserJoined != null) {
@ -480,4 +482,48 @@ class RTCManager {
if (!removed) return;
remoteUsersNotifier.value = List<int>.unmodifiable(_remoteUserIds);
}
///
Future<void> publishVideo(CurrentRole role) async {
await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await _engine?.muteLocalAudioStream(false);
await _engine?.muteLocalVideoStream(false);
await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '',
message: json.encode({
'type': 'join_chat',
'uid': _currentUid,
'role': role == CurrentRole.maleAudience
? 'male_audience'
: 'female_audience',
}),
);
}
///
Future<void> publishAudio() async {
await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await _engine?.muteLocalAudioStream(false);
await _engine?.muteLocalVideoStream(true);
await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '',
message: json.encode({
'type': 'join_chat',
'uid': _currentUid,
'role': 'audience',
}),
);
}
///
Future<void> unpublish(CurrentRole role) async {
await _engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
await _engine?.muteLocalAudioStream(true);
await _engine?.muteLocalVideoStream(true);
await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '',
message: json.encode({'type': 'leave_chat', 'uid': _currentUid, 'role': role == CurrentRole.maleAudience
? 'male_audience' : 'female_audience',}),
);
}
}

16
lib/rtc/rtm_manager.dart

@ -63,7 +63,7 @@ class RTMManager {
await dispose();
final (status, client) = await RTM(appId, userId, config: config);
print('RTM初始化成功');
print(status.error ? 'RTM 初始化失败' : '✅ RTM 初始化成功');
if (status.error) {
onOperationError?.call(status);
@ -74,13 +74,13 @@ class RTMManager {
_currentUserId = userId;
_isInitialized = true;
_registerClientListeners();
// final response = await _networkService.rtcApi.getSwRtmToken();
// //
// if (response.data.isSuccess) {
await login('007eJxTYIhoZ/m/gMf0gdWv1PV2R6I/Lpry06W77sOR2BDuFv89JeEKDCbJRqmJlinJSSbJpiYmBqaWxokmhhYpaQZpKSmmhkYpE9/IZgrwMTDYOB4rZ2RgYmAEQhCfkcEcAPTmHg4=');
// } else {
// SmartDialog.showToast(response.data.message);
// }
final response = await _networkService.rtcApi.getSwRtmToken();
//
if (response.data.isSuccess) {
await login(response.data.data!.token);
} else {
SmartDialog.showToast(response.data.message);
}
return true;
}

9
lib/service/live_chat_message_service.dart

@ -1,11 +1,13 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:agora_rtm/agora_rtm.dart';
import 'package:dating_touchme_app/controller/discover/room_controller.dart';
import 'package:dating_touchme_app/controller/global.dart';
import 'package:dating_touchme_app/model/live/live_chat_message.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
import 'package:dating_touchme_app/rtc/rtm_manager.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
///
@ -56,16 +58,19 @@ class LiveChatMessageService {
}
///
void _handleIncomingMessage(MessageEvent event) {
void _handleIncomingMessage(MessageEvent event) async{
try {
//
final messageText = _parseMessageContent(event.message);
final messageData = json.decode(messageText) as Map<String, dynamic>;
print('📥 收到消息: $messageData');
//
if (messageData['type'] == 'chat_message') {
final chatMessage = LiveChatMessage.fromJson(messageData);
onMessageReceived?.call(chatMessage);
}else{
RoomController controller = Get.find<RoomController>();
await controller.receiveRTCMessage(messageData);
}
} catch (e, stackTrace) {
final error = '解析RTM消息失败: $e';

165
lib/widget/live/live_room_anchor_showcase.dart

@ -1,8 +1,12 @@
import 'package:agora_rtc_engine/agora_rtc_engine.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/rtc/rtc_channel_detail.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class LiveRoomAnchorShowcase extends StatefulWidget {
const LiveRoomAnchorShowcase({super.key});
@ -13,6 +17,7 @@ class LiveRoomAnchorShowcase extends StatefulWidget {
class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
final RTCManager _rtcManager = RTCManager.instance;
final RoomController _roomController = Get.find<RoomController>();
@override
Widget build(BuildContext context) {
@ -94,19 +99,25 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
],
),
SizedBox(height: 5.w),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildSideAnchorCard(
isLeft: true,
micIcon: Assets.imagesMicClose,
),
_buildSideAnchorCard(
isLeft: false,
micIcon: Assets.imagesMicOpen,
),
],
),
Obx(() {
// 访
_roomController.rtcChannelDetail.value;
final rtcChannelDetail =
_roomController.rtcChannelDetail.value;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildSideAnchorCard(
isLeft: true,
userInfo: rtcChannelDetail?.maleInfo,
),
_buildSideAnchorCard(
isLeft: false,
userInfo: rtcChannelDetail?.femaleInfo,
),
],
);
}),
],
);
},
@ -134,12 +145,14 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
canvas: const VideoCanvas(uid: 0),
),
)
: (_rtcManager.currentChannelId == null
: (_rtcManager.currentChannelId == null && _rtcManager.remoteUserIds.isEmpty
? _buildWaitingPlaceholder()
: AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: engine,
canvas: VideoCanvas(uid: _rtcManager.remoteUserIds.first),
canvas: VideoCanvas(
uid: _rtcManager.remoteUserIds.first,
),
connection: RtcConnection(
channelId: _rtcManager.currentChannelId!,
),
@ -169,17 +182,64 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
);
}
Widget _buildSideAnchorCard({required bool isLeft, required String micIcon}) {
Widget _buildSideAnchorCard({
required bool isLeft,
RtcSeatUserInfo? userInfo,
}) {
final engine = _rtcManager.engine;
final joined = _rtcManager.channelJoinedNotifier.value;
//
final bool isCurrentUser =
_roomController.currentRole == CurrentRole.maleAudience ||
_roomController.currentRole == CurrentRole.femaleAudience;
return Stack(
children: [
Container(
width: 177.w,
height: 175.w,
decoration: BoxDecoration(
borderRadius: isLeft
? BorderRadius.horizontal(left: Radius.circular(18.w))
: BorderRadius.horizontal(right: Radius.circular(18.w)),
color: const Color.fromRGBO(47, 10, 94, 1),
ClipRRect(
borderRadius: isLeft
? BorderRadius.horizontal(left: Radius.circular(18.w))
: BorderRadius.horizontal(right: Radius.circular(18.w)),
child: SizedBox(
width: 177.w,
height: 175.w,
child:
userInfo != null &&
userInfo.uid != null &&
joined &&
engine != null
? AgoraVideoView(
controller: isCurrentUser
? VideoViewController(
rtcEngine: engine,
canvas: const VideoCanvas(uid: 0),
)
: VideoViewController.remote(
rtcEngine: engine,
canvas: VideoCanvas(uid: userInfo.uid!),
connection: RtcConnection(
channelId: _rtcManager.currentChannelId ?? '',
),
),
)
: Container(
decoration: BoxDecoration(
borderRadius: isLeft
? BorderRadius.horizontal(left: Radius.circular(18.w))
: BorderRadius.horizontal(
right: Radius.circular(18.w),
),
color: const Color.fromRGBO(47, 10, 94, 1),
),
child: Center(
child: Text(
'等待${isLeft ? "" : ""}嘉宾',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12.w,
),
),
),
),
),
),
Positioned(
@ -222,34 +282,41 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
),
),
),
Positioned(
left: 5.w,
bottom: 5.w,
child: Row(
children: [
Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4.w)),
color: const Color.fromRGBO(0, 0, 0, .65),
),
child: Center(
child: Image.asset(micIcon, width: 10.w, height: 11.w),
if (userInfo != null)
Positioned(
left: 5.w,
bottom: 5.w,
child: Row(
children: [
Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4.w)),
color: const Color.fromRGBO(0, 0, 0, .65),
),
child: Center(
child: Image.asset(
userInfo.isMicrophoneOn
? Assets.imagesMicOpen
: Assets.imagesMicClose,
width: 10.w,
height: 11.w,
),
),
),
),
SizedBox(width: 5.w),
Text(
"飞翔的企鹅",
style: TextStyle(
fontSize: 11.w,
color: Colors.white,
fontWeight: FontWeight.w500,
SizedBox(width: 5.w),
Text(
userInfo.nickName,
style: TextStyle(
fontSize: 11.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
),
),
],
);
}

69
lib/widget/live/live_room_notice_chat_panel.dart

@ -1,4 +1,6 @@
import 'package:dating_touchme_app/controller/discover/room_controller.dart';
import 'package:dating_touchme_app/controller/global.dart';
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/widget/live/live_room_chat_item.dart';
import 'package:flutter/material.dart';
@ -9,7 +11,8 @@ class LiveRoomNoticeChatPanel extends StatefulWidget {
const LiveRoomNoticeChatPanel({super.key});
@override
State<LiveRoomNoticeChatPanel> createState() => _LiveRoomNoticeChatPanelState();
State<LiveRoomNoticeChatPanel> createState() =>
_LiveRoomNoticeChatPanelState();
}
class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
@ -30,7 +33,6 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
@override
Widget build(BuildContext context) {
return Container(
height: 230.w,
padding: EdgeInsets.only(left: 13.w, right: 9.w),
@ -73,14 +75,65 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
}),
),
SizedBox(width: 18.w),
Image.asset(
Assets.imagesAd,
width: 73.w,
fit: BoxFit.cover,
),
Obx((){
if(controller.rtcChannelDetail.value?.maleInfo == null && GlobalData().userData?.genderCode == 0 && controller.currentRole != CurrentRole.broadcaster ||
controller.rtcChannelDetail.value?.femaleInfo == null && GlobalData().userData?.genderCode == 1 && controller.currentRole != CurrentRole.broadcaster ||
controller.isLive && controller.currentRole != CurrentRole.broadcaster){
return Container(
width: 120.w,
height: 55.w,
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.w),
gradient: LinearGradient(
colors: controller.isLive ? [Colors.grey, Colors.grey] : [Color(0xFF7C63FF), Color(0xFF987CFF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [
Image.asset(
Assets.imagesRoomVideo,
width: 26.w,
),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
controller.isLive ? '申请连麦中' : '免费连麦',
style: TextStyle(
fontSize: 13.w,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 2.w),
controller.isLive ? const SizedBox() :Text(
'剩余2张相亲卡',
style: TextStyle(
fontSize: 9.w,
color: Colors.white.withOpacity(0.8),
),
),
],
),
],
),
).onTap(() async{
if(controller.isLive){
await controller.leaveChat();
}else{
await controller.joinChat(GlobalData().userData?.genderCode == 0 ? CurrentRole.maleAudience : CurrentRole.femaleAudience);
}
});
}
return const SizedBox();
}),
],
),
);
}
}

50
lib/widget/live/live_room_user_header.dart

@ -39,8 +39,7 @@ class LiveRoomUserHeader extends StatelessWidget {
child: Row(
children: [
//
avatarAsset.startsWith('http://') || avatarAsset.startsWith('https://')
? ClipOval(
ClipOval(
child: Image.network(
avatarAsset,
width: 34.w,
@ -54,11 +53,6 @@ class LiveRoomUserHeader extends StatelessWidget {
);
},
),
)
: Image.asset(
avatarAsset,
width: 34.w,
height: 34.w,
),
SizedBox(width: 7.w),
Column(
@ -72,50 +66,8 @@ class LiveRoomUserHeader extends StatelessWidget {
color: Colors.white,
),
),
SizedBox(height: 2.w),
Row(
children: [
Image.asset(
fireIconAsset,
width: 10.w,
height: 12.w,
),
SizedBox(width: 4.w),
Text(
popularityText,
style: TextStyle(
fontSize: 10.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
SizedBox(width: 15.w),
GestureDetector(
onTap: onFollowTap,
child: Container(
width: 47.w,
height: 27.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(27.w)),
color: const Color.fromRGBO(253, 43, 84, 1),
),
child: Center(
child: Text(
'关注',
style: TextStyle(
fontSize: 13.w,
color: Colors.white,
fontWeight: FontWeight.w500,
height: 1,
),
),
),
),
),
],
),
),

68
location_plugin/example/pubspec.lock

@ -6,7 +6,7 @@ packages:
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.13.0"
boolean_selector:
@ -14,7 +14,7 @@ packages:
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
characters:
@ -22,7 +22,7 @@ packages:
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
clock:
@ -30,7 +30,7 @@ packages:
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
collection:
@ -38,7 +38,7 @@ packages:
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.1"
cupertino_icons:
@ -46,7 +46,7 @@ packages:
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.8"
fake_async:
@ -54,7 +54,7 @@ packages:
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.3"
file:
@ -62,7 +62,7 @@ packages:
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
flutter:
@ -80,7 +80,7 @@ packages:
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
flutter_test:
@ -98,7 +98,7 @@ packages:
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.6.0"
http_parser:
@ -106,7 +106,7 @@ packages:
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
integration_test:
@ -119,7 +119,7 @@ packages:
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
@ -127,7 +127,7 @@ packages:
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.10"
leak_tracker_testing:
@ -135,7 +135,7 @@ packages:
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
lints:
@ -143,7 +143,7 @@ packages:
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.1"
location_plugin:
@ -158,7 +158,7 @@ packages:
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.17"
material_color_utilities:
@ -166,7 +166,7 @@ packages:
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.11.1"
meta:
@ -174,7 +174,7 @@ packages:
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.16.0"
path:
@ -182,7 +182,7 @@ packages:
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
platform:
@ -190,7 +190,7 @@ packages:
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface:
@ -198,7 +198,7 @@ packages:
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
process:
@ -206,7 +206,7 @@ packages:
description:
name: process
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.5"
sky_engine:
@ -219,7 +219,7 @@ packages:
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.1"
stack_trace:
@ -227,7 +227,7 @@ packages:
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.1"
stream_channel:
@ -235,7 +235,7 @@ packages:
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
string_scanner:
@ -243,7 +243,7 @@ packages:
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
sync_http:
@ -251,7 +251,7 @@ packages:
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.1"
term_glyph:
@ -259,7 +259,7 @@ packages:
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
test_api:
@ -267,7 +267,7 @@ packages:
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.6"
typed_data:
@ -275,7 +275,7 @@ packages:
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
vector_math:
@ -283,7 +283,7 @@ packages:
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
vm_service:
@ -291,7 +291,7 @@ packages:
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "15.0.2"
web:
@ -299,7 +299,7 @@ packages:
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
webdriver:
@ -307,7 +307,7 @@ packages:
description:
name: webdriver
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
sdks:

Loading…
Cancel
Save