diff --git a/assets/images/group.png b/assets/images/group.png new file mode 100644 index 0000000..30cf668 Binary files /dev/null and b/assets/images/group.png differ diff --git a/lib/IM/controller/chat_detail_controller.dart b/lib/IM/controller/chat_detail_controller.dart index de461e8..46d7369 100644 --- a/lib/IM/controller/chat_detail_controller.dart +++ b/lib/IM/controller/chat_detail_controller.dart @@ -6,9 +6,9 @@ import 'package:loopin/utils/index.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; class ChatDetailController extends GetxController { - final String userID; + final String id; - ChatDetailController({required this.userID}); + ChatDetailController({required this.id}); final ScrollController chatController = ScrollController(); final RxList chatList = [].obs; diff --git a/lib/IM/controller/im_user_info_controller.dart b/lib/IM/controller/im_user_info_controller.dart index 95cc7fd..5d6a3b0 100644 --- a/lib/IM/controller/im_user_info_controller.dart +++ b/lib/IM/controller/im_user_info_controller.dart @@ -24,6 +24,7 @@ class ImUserInfoController extends GetxController { "area": "", "areaCode": "", "openId": "", + "tag": "", }.obs; final role = 0.obs; final level = 0.obs; @@ -44,6 +45,7 @@ class ImUserInfoController extends GetxController { "area": "", "areaCode": "", "openId": "", + "tag": "", }); role.value = userInfo.role ?? 0; diff --git a/lib/IM/im_message_listeners.dart b/lib/IM/im_message_listeners.dart index 921ba6a..557f55f 100644 --- a/lib/IM/im_message_listeners.dart +++ b/lib/IM/im_message_listeners.dart @@ -71,24 +71,26 @@ class ImMessageListenerService extends GetxService { /// 处理消息 void _handleNewMessage(V2TimMessage message) async { - final userID = message.sender ?? ''; - if (userID.isEmpty) return; + final id = message.sender ?? message.groupID ?? ''; + final isGroup = message.groupID != null && message.groupID!.isNotEmpty; + final conversationID = isGroup ? 'group_${message.groupID}' : 'c2c_${message.userID}'; + if (id.isEmpty) return; /// 是否正在聊天 优先处理 if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') && Get.isRegistered()) { final chatDetailController = Get.find(); - // 单聊的处理 - if (chatDetailController.userID == userID) { + // 单聊和群聊的处理(chatDetailController.id是通过chat_binding传入的,按顺序取userID,没有则取groupID) + if (chatDetailController.id == id) { // 确认正在聊天,插入消息前检测是否需要打时间标签 insertTimeLabel(message); // 标注为已读 - await ImService.instance.clearConversationUnreadCount(conversationID: 'c2c_$userID'); + await ImService.instance.clearConversationUnreadCount(conversationID: conversationID); } return; } // 未杀死状态,交给在线推送处理 if (!LifecycleHandler.isInForeground) { - logger.i("App 当前在后台,收到来自 $userID 的消息 ${message.msgID},暂不展示弹窗"); + logger.i("App 当前在后台,收到来自 $id 的消息 ${message.msgID},暂不展示弹窗"); // return; } //TODO 过滤有管理员标识的信息 diff --git a/lib/IM/im_service.dart b/lib/IM/im_service.dart index 12cbe9c..718b6a1 100644 --- a/lib/IM/im_service.dart +++ b/lib/IM/im_service.dart @@ -17,6 +17,9 @@ import 'package:tencent_cloud_chat_sdk/enum/friend_application_type_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/friend_response_type_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/friend_type_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/group_add_opt_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_application_type_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_member_role_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/history_msg_get_type_enum.dart'; import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; @@ -30,7 +33,17 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.da import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_operation_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_application.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_application_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_operation_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_param.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_search_param.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_change_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_search_param.dart'; @@ -40,6 +53,8 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_info_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_conversation_manager.dart'; import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_friendship_manager.dart'; +import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_group_manager.dart'; +import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_manager.dart'; import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_message_manager.dart'; import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; @@ -734,6 +749,7 @@ class ImService { return ImResult.wrap(res); } + ///------------------------------------ /// 创建群 Future> createGroup({ String? groupID, @@ -768,4 +784,299 @@ class ImService { return ImResult.wrap(res); } + + /// 获取当前用户已经加入的群列表 + /// 注意: + /// - 直播群(AVChatRoom) 不支持该 API + /// - SDK 限制调用频率为 1 秒 10 次,超过限制后会报错 7008 + Future>> getJoinedGroupList() async { + final res = await V2TIMGroupManager().getJoinedGroupList(); + return ImResult.wrap(res); + } + + /// 申请加入群(不包含work群,work群只能邀请进群) + Future> joinGroup({ + required String groupID, + required String message, + }) async { + final res = await TIMManager.instance.joinGroup( + groupID: groupID, + message: message, + ); + + return ImResult.wrapNoData(res); + } + + /// 拉取群资料 + /// 参数: + /// - [groupIDList] 群 ID 列表 + Future>> getGroupsInfo({ + required List groupIDList, + }) async { + final res = await V2TIMGroupManager().getGroupsInfo(groupIDList: groupIDList); + return ImResult.wrap(res); + } + + /// 修改群资料 + /// + /// 参数: + /// - [info] 群资料参数 + Future> setGroupInfo({ + required V2TimGroupInfo info, + }) async { + final res = await V2TIMGroupManager().setGroupInfo(info: info); + return ImResult.wrapNoData(res); + } + + /// 获取群成员列表 + /// + /// 参数说明: + /// [groupID] 群 ID + /// [filter] 成员过滤类型: + /// - V2TIM_GROUP_MEMBER_FILTER_ALL:所有类型 + /// - V2TIM_GROUP_MEMBER_FILTER_OWNER:群主 + /// - V2TIM_GROUP_MEMBER_FILTER_ADMIN:群管理员 + /// - V2TIM_GROUP_MEMBER_FILTER_COMMON:普通群成员 + /// [nextSeq] 分页拉取标志,首次传 0 + /// [count] 拉取数量,最大 100 + /// [offset] 偏移量(仅 Web 端需要) + Future> getGroupMemberList({ + required String groupID, + required GroupMemberFilterTypeEnum filter, + required String nextSeq, + int count = 20, + int offset = 0, + }) async { + final res = await V2TIMGroupManager().getGroupMemberList( + groupID: groupID, + filter: filter, + nextSeq: nextSeq, + count: count, + offset: offset, + ); + return ImResult.wrap(res); + } + + /// 获取指定的群成员资料 + /// + /// [groupID] 群 ID + /// [memberList] 需要获取资料的成员 ID 列表 + Future>> getGroupMembersInfo({ + required String groupID, + required List memberList, + }) async { + final res = await V2TIMGroupManager().getGroupMembersInfo( + groupID: groupID, + memberList: memberList, + ); + return ImResult.wrap(res); + } + + /// 修改指定的群成员资料 + /// + /// [groupID] 群 ID + /// [userID] 成员 ID + /// [nameCard] 群名片(昵称) + /// [customInfo] 自定义字段 + Future> setGroupMemberInfo({ + required String groupID, + required String userID, + String? nameCard, + Map? customInfo, + }) async { + final res = await V2TIMGroupManager().setGroupMemberInfo( + groupID: groupID, + userID: userID, + nameCard: nameCard, + customInfo: customInfo, + ); + return ImResult.wrapNoData(res); + } + + /// 邀请他人入群(work群的入群方式,任何人可邀请人入群,无法做权限设置) + /// + /// [groupID] 群 ID + /// [userList] 被邀请成员的 userID 列表 + /// + /// 注意: + /// + /// ``` + /// 工作群(Work):群里的任何人都可以邀请其他人进群。 + /// 会议群(Meeting)和公开群(Public):只有通过 REST API 使用 App 管理员身份才可以邀请其他人进群。 + /// 直播群(AVChatRoom):不支持此功能。 + /// ``` + Future>> inviteUserToGroup({ + required String groupID, + required List userList, + }) async { + final res = await TIMGroupManager.instance.inviteUserToGroup( + groupID: groupID, + userList: userList, + ); + return ImResult.wrap(res); + } + + /// 踢人 + /// + /// [groupID] 群 ID + /// [memberList] 被踢成员的 userID 列表 + /// [duration] 禁言时长,单位秒(可选,仅对禁言有效) + /// [reason] 踢人的原因(可选) + /// + /// 注意: + /// ``` + /// 工作群(Work):只有群主或 APP 管理员可以踢人。 + /// 公开群(Public)、会议群(Meeting):群主、管理员和 APP 管理员可以踢人。 + /// 直播群(AVChatRoom):只支持禁言(muteGroupMember),不支持踢人。 + /// ``` + Future> kickGroupMember({ + required String groupID, + required List memberList, + int? duration, + String? reason, + }) async { + final res = await TIMGroupManager.instance.kickGroupMember( + groupID: groupID, + memberList: memberList, + duration: duration, + reason: reason, + ); + return ImResult.wrapNoData(res); + } + + /// 切换群成员的角色 + /// + /// [groupID] 群 ID + /// [userID] 被切换角色的成员 userID + /// [role] 目标角色 + /// + /// 注意: + /// ``` + /// 公开群(Public)和会议群(Meeting):只有群主才能对群成员进行普通成员和管理员之间的角色切换。 + /// 其他群不支持设置群成员角色。 + /// 转让群组请调用 transferGroupOwner 接口。 + /// ``` + Future> setGroupMemberRole({ + required String groupID, + required String userID, + required GroupMemberRoleTypeEnum role, + }) async { + final res = await TIMGroupManager.instance.setGroupMemberRole( + groupID: groupID, + userID: userID, + role: role, + ); + return ImResult.wrapNoData(res); + } + + /// 转让群主 + /// + /// [groupID] 群 ID + /// [userID] 新群主的 userID + /// + /// 注意: + /// ``` + /// 普通类型的群(Work、Public、Meeting):只有群主才有权限进行群转让操作。 + /// 直播群(AVChatRoom):不支持转让群主。 + /// ``` + Future> transferGroupOwner({ + required String groupID, + required String userID, + }) async { + final res = await TIMGroupManager.instance.transferGroupOwner( + groupID: groupID, + userID: userID, + ); + return ImResult.wrapNoData(res); + } + + /// 获取加群的申请列表 + Future> getGroupApplicationList() async { + final res = await TIMGroupManager.instance.getGroupApplicationList(); + return ImResult.wrap(res); + } + + /// 同意某一条加群申请 + Future> acceptGroupApplication({ + required String groupID, + String? reason, + required String fromUser, + required String toUser, // 处理这条数据的人,管理员或者群主 + int? addTime, + GroupApplicationTypeEnum? type, + V2TimGroupApplication? application, + String? webMessageInstance, + }) async { + final res = await TIMGroupManager.instance.acceptGroupApplication( + groupID: groupID, + reason: reason, + fromUser: fromUser, + toUser: toUser, + addTime: addTime, + type: type, + application: application, + ); + + return ImResult.wrapNoData(res); + } + + /// 拒绝某一条加群申请 + Future refuseGroupApplication({ + required String groupID, + String? reason, + required String fromUser, + required String toUser, + required int addTime, + required GroupApplicationTypeEnum type, + V2TimGroupApplication? application, + }) async { + final res = await TIMGroupManager.instance.refuseGroupApplication( + groupID: groupID, + fromUser: fromUser, + toUser: toUser, + addTime: addTime, + type: type, + application: application, + ); + + return ImResult.wrapNoData(res); + } + + /// 标记加群申请列表为已读 + Future setGroupApplicationRead() async { + final res = await TIMGroupManager.instance.setGroupApplicationRead(); + return ImResult.wrapNoData(res); + } + + /// 搜索本地群组资料 + Future>> searchGroups({ + required V2TimGroupSearchParam searchParam, + }) async { + final res = await TIMGroupManager.instance.searchGroups(searchParam: searchParam); + return ImResult.wrap(res); + } + + /// 搜索本地群成员 + Future> searchGroupMembers({ + required V2TimGroupMemberSearchParam param, + }) async { + final res = await TIMGroupManager.instance.searchGroupMembers(param: param); + return ImResult.wrap(res); + } + + /// 解散群(Work:任何人都无法解散群组,其他群:群主可以解散群组) + Future> dismissGroup({ + required String groupID, + }) async { + final res = await TIMManager.instance.dismissGroup(groupID: groupID); + return ImResult.wrapNoData(res); + } + + /// 退群 ,在公开群(Public)、会议(Meeting)和直播群(AVChatRoom)中,群主是不可以退群的,群主只能调用 dismissGroup 解散群组。 + Future> quitGroup({ + required String groupID, + }) async { + final res = await TIMManager.instance.quitGroup(groupID: groupID); + return ImResult.wrapNoData(res); + } } diff --git a/lib/bings/chat_binding.dart b/lib/bings/chat_binding.dart index dbf8ac0..e4c2b2c 100644 --- a/lib/bings/chat_binding.dart +++ b/lib/bings/chat_binding.dart @@ -6,6 +6,7 @@ class ChatBinding extends Bindings { @override void dependencies() { V2TimConversation conversation = Get.arguments; - Get.put(ChatDetailController(userID: conversation.userID!)); + final id = conversation.userID ?? conversation.groupID; + Get.put(ChatDetailController(id: id!)); } } diff --git a/lib/controller/shop_index_controller.dart b/lib/controller/shop_index_controller.dart index bbf1b2e..a094c1b 100644 --- a/lib/controller/shop_index_controller.dart +++ b/lib/controller/shop_index_controller.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:loopin/IM/im_friend_listeners.dart'; import 'package:loopin/api/shop_api.dart'; import 'package:loopin/service/http.dart'; @@ -117,7 +118,7 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta }); final data = res['data']['records']; - print('商品返回数据------------------------->${data}'); + logger.w('商品返回数据:$index------------------------->$data'); tab.dataList.addAll(data); // logger.w(res); @@ -132,7 +133,7 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta 'type': 1, }); final data = res['data']; - // logger.w(res); + logger.w(res); swiperData.assignAll(data); } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index b6a1b93..0a94923 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2,23 +2,28 @@ library; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:loopin/IM/controller/chat_detail_controller.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_result.dart'; import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/components/image_viewer.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/preview_video.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/utils/audio_player_service.dart'; import 'package:loopin/utils/snapshot.dart'; import 'package:loopin/utils/voice_service.dart'; +import 'package:mime/mime.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +import 'package:video_player/video_player.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import '../../styles/index.dart'; @@ -36,6 +41,8 @@ class Chat extends StatefulWidget { } class _ChatState extends State with SingleTickerProviderStateMixin { + final ImagePicker _picker = ImagePicker(); + late final ChatDetailController controller; // 接收参数 late final Rx arguments; @@ -346,6 +353,13 @@ class _ChatState extends State with SingleTickerProviderStateMixin { data: item, child: Ink( child: InkWell( + onTap: () { + // 预览图片 + Get.to(() => ImageViewer( + images: [imagePaths.first], + index: 0, + )); + }, overlayColor: WidgetStateProperty.all(Colors.transparent), child: ClipRRect( borderRadius: BorderRadius.circular(10.0), @@ -525,9 +539,18 @@ class _ChatState extends State with SingleTickerProviderStateMixin { } // 分享团购商品 else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareTuangou) { - //price,title,url,sell + // final makeJson = jsonEncode({ + // "price": shopObj['price'], + // "title": shopObj['name'], + // "url": shopObj['pic'], + // "sell": Utils.graceNumber(int.parse(shopObj['sales'] ?? '0')), + // "goodsId": shopObj['id'], + // "userID": Get.find().userID.value, + // }); final obj = jsonDecode(item.customElem!.data!); logger.e(obj); + final goodsId = obj['goodsId']; + final userID = obj['userID']; final url = obj['url']; final title = obj['title']; final price = obj['price']; @@ -535,6 +558,10 @@ class _ChatState extends State with SingleTickerProviderStateMixin { msgtpl.add(RenderChatItem( data: item, child: GestureDetector( + onTap: () { + // 这里带上分享人的ID + Get.toNamed('/goods', arguments: {'goodsId': goodsId, 'userID': userID}); + }, child: Container( width: 160, clipBehavior: Clip.antiAlias, @@ -596,11 +623,6 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ], ), ), - onTap: () { - // 这里带上分享人的ID - // Get.toNamed('/goods'); - Get.toNamed('/goods', arguments: {}); - }, ), )); } @@ -609,10 +631,12 @@ class _ChatState extends State with SingleTickerProviderStateMixin { /// {imgUrl,videoUrl,width,height} final obj = jsonDecode(item.customElem!.data!); logger.e(obj); + final videoId = obj['videoId']; final videoUrl = obj['videoUrl']; final imgUrl = obj['imgUrl']; final width = obj['width'] as num; final height = obj['height'] as num; + final isHorizontal = width > height; msgtpl.add(RenderChatItem( data: item, child: Ink( @@ -625,9 +649,15 @@ class _ChatState extends State with SingleTickerProviderStateMixin { children: [ ClipRRect( borderRadius: BorderRadius.circular(10.0), - child: NetworkOrAssetImage( - imageUrl: imgUrl, + child: Container( width: 120, + height: 240, + color: Colors.black, + child: NetworkOrAssetImage( + imageUrl: imgUrl, + fit: isHorizontal ? BoxFit.contain : BoxFit.cover, + placeholderAsset: 'assets/images/bk.jpg', + ), ), ), const Align( @@ -642,31 +672,32 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), ), onTap: () { - showGeneralDialog( - context: context, - barrierColor: Colors.black.withAlpha((1.0 * 255).round()), - pageBuilder: (_, __, ___) { - return SafeArea( - bottom: true, - child: Padding( - padding: const EdgeInsets.only(bottom: 4), - child: PreviewVideo( - videoUrl: videoUrl, - width: width.toDouble(), - height: height.toDouble(), - ), - ), - ); - }, - transitionBuilder: (_, anim, __, child) { - return FadeTransition(opacity: anim, child: child); - }, - transitionDuration: const Duration(milliseconds: 200), - ); - }, - onLongPress: () { - contextMenuDialog(); + Get.toNamed('/videoDetail', arguments: {'videoId': videoId}); + // showGeneralDialog( + // context: context, + // barrierColor: Colors.black.withAlpha((1.0 * 255).round()), + // pageBuilder: (_, __, ___) { + // return SafeArea( + // bottom: true, + // child: Padding( + // padding: const EdgeInsets.only(bottom: 4), + // child: PreviewVideo( + // videoUrl: videoUrl, + // width: width.toDouble(), + // height: height.toDouble(), + // ), + // ), + // ); + // }, + // transitionBuilder: (_, anim, __, child) { + // return FadeTransition(opacity: anim, child: child); + // }, + // transitionDuration: const Duration(milliseconds: 200), + // ); }, + // onLongPress: () { + // contextMenuDialog(); + // }, ), ), )); @@ -1022,7 +1053,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { } // 发送消息队列 - void sendMessage(message) async { + Future sendMessage(message) async { // 待插入的消息 List messagesToInsert = []; V2TimMessage? lastRealMsg; @@ -1192,6 +1223,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin { // 发送视频消息=5 void sendVideo(videoFilePath, type, duration, snapshotPath) async { + final instance = MyDialog.loading('处理中'); + final resImg = await IMMessage().createVideoMessage( videoFilePath: videoFilePath, type: type, @@ -1199,13 +1232,116 @@ class _ChatState extends State with SingleTickerProviderStateMixin { snapshotPath: snapshotPath, ); if (resImg.success) { - sendMessage(resImg.data?.messageInfo); + await sendMessage(resImg.data?.messageInfo); + instance.close(); } } + // 拍摄 + Future showPicker(BuildContext context) async { + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, // 透明底色,这样圆角才能生效 + + builder: (context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () async { + Navigator.pop(context); + await _pickPhoto(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.photo), + SizedBox(width: 8), + Text("拍摄照片"), + ], + ), + ), + ), + InkWell( + onTap: () async { + Navigator.pop(context); + await _pickVideo(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.videocam), + SizedBox(width: 8), + Text("拍摄视频"), + ], + ), + ), + ), + const Divider(height: 1), + InkWell( + onTap: () => Navigator.pop(context), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + // Icon(Icons.close), + // SizedBox(width: 8), + Text("取消"), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + /// 拍照片 + Future _pickPhoto() async { + final XFile? photo = await _picker.pickImage( + source: ImageSource.camera, + maxWidth: 1920, + maxHeight: 1080, + imageQuality: 90, + ); + if (photo != null) { + logger.w("照片路径: ${photo.path}"); + sendImage(photo.path); + } + } + + /// 拍视频 + Future _pickVideo() async { + final XFile? video = await _picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + + if (video == null) return; + logger.w("视频路径: ${video.path}"); + // 获取首帧图 + final snapshot = await generateVideoThumbnail(video.path) ?? ''; + // 获取视频时长 + final controller = VideoPlayerController.file(File(video.path)); + await controller.initialize(); + final duration = controller.value.duration.inSeconds; + await controller.dispose(); + // mimeType + final mimeType = lookupMimeType(video.path) ?? 'video/mp4'; + sendVideo(video.path, mimeType, duration, snapshot); + } + // 底部操作蓝选择区操作 void handleChooseAction(key) { - MyDialog.toast('$key'); + // MyDialog.toast('$key'); switch (key) { case 'photo': // .... @@ -1213,6 +1349,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { break; case 'camera': // .... + showPicker(context); break; case 'redpacket': sendRedPacketDialog(); @@ -1421,35 +1558,35 @@ class _ChatState extends State with SingleTickerProviderStateMixin { // 长按消息菜单 void contextMenuDialog() { - showDialog( - context: context, - builder: (context) { - return SimpleDialog( - backgroundColor: Colors.white, - surfaceTintColor: Colors.white, - contentPadding: const EdgeInsets.symmetric(vertical: 5.0), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), - children: [ - SimpleDialogOption( - child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('复制')), - onPressed: () {}, - ), - SimpleDialogOption( - child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('发送给朋友')), - onPressed: () {}, - ), - SimpleDialogOption( - child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('收藏')), - onPressed: () {}, - ), - SimpleDialogOption( - child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('删除')), - onPressed: () {}, - ), - ], - ); - }, - ); + // showDialog( + // context: context, + // builder: (context) { + // return SimpleDialog( + // backgroundColor: Colors.white, + // surfaceTintColor: Colors.white, + // contentPadding: const EdgeInsets.symmetric(vertical: 5.0), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), + // children: [ + // SimpleDialogOption( + // child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('复制')), + // onPressed: () {}, + // ), + // SimpleDialogOption( + // child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('发送给朋友')), + // onPressed: () {}, + // ), + // SimpleDialogOption( + // child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('收藏')), + // onPressed: () {}, + // ), + // SimpleDialogOption( + // child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('删除')), + // onPressed: () {}, + // ), + // ], + // ); + // }, + // ); } // 发群红包弹窗 diff --git a/lib/pages/chat/chat_group.dart b/lib/pages/chat/chat_group.dart index 6d555f9..ca5b4ae 100644 --- a/lib/pages/chat/chat_group.dart +++ b/lib/pages/chat/chat_group.dart @@ -1,31 +1,34 @@ -/// 聊天模板 +/// 群聊模板 library; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:loopin/IM/controller/chat_detail_controller.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_result.dart'; import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/components/image_viewer.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/preview_video.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/utils/audio_player_service.dart'; import 'package:loopin/utils/snapshot.dart'; import 'package:loopin/utils/voice_service.dart'; +import 'package:mime/mime.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +import 'package:video_player/video_player.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import '../../styles/index.dart'; import '../../utils/index.dart'; import './components/redpacket.dart'; import './components/richtext.dart'; -// import 'mock/chat_json.dart'; import 'mock/emoj_json.dart'; class ChatGroup extends StatefulWidget { @@ -36,9 +39,11 @@ class ChatGroup extends StatefulWidget { } class _ChatGroupState extends State with SingleTickerProviderStateMixin { + final ImagePicker _picker = ImagePicker(); + late final ChatDetailController controller; // 接收参数 - late V2TimConversation arguments; + final V2TimConversation arguments = Get.arguments; late String selfUserId; @@ -91,7 +96,6 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix @override void initState() { super.initState(); - arguments = Get.arguments; controller = Get.find(); chatController = controller.chatController; @@ -146,56 +150,6 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix super.dispose(); } - // 设置好友备注 - void setRemark() async { - String remark = ''; - await MyDialog.confirm( - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - TextField( - onChanged: (value) => remark = value, - maxLength: 16, - maxLengthEnforcement: MaxLengthEnforcement.enforced, // 强制不能输入超过 - decoration: InputDecoration( - hintText: '请输入备注', - filled: true, - fillColor: const Color(0xFFF5F5F5), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Color(0xFFBE4EFF), width: 1), - ), - ), - ) - ], - ), - title: '设置备注', - buttonText: '确认', - cancelText: '取消', - onConfirm: () async { - // print('备注为:$remark'); - final res = await ImService.instance.setFriendInfo(userID: arguments.userID!, friendRemark: remark); - if (res.success) { - setState(() { - arguments.showName = remark; - }); - } else { - print(res.desc); - print(arguments.userID); - MyDialog.toast(res.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); - } - return true; - }, - ); - } - void cleanUnRead() async { if ((arguments.unreadCount ?? 0) > 0) { final res = await ImService.instance.clearConversationUnreadCount(conversationID: arguments.conversationID); @@ -219,7 +173,6 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix // 获取最旧一条消息作为游标 V2TimMessage? lastRealMsg; - // for (var msg in controller.chatList.reversed) { for (var msg in controller.chatList.reversed) { if (msg.localCustomData != 'time_label') { lastRealMsg = msg; @@ -227,12 +180,10 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix } } final lastMsg = lastRealMsg ?? arguments.lastMessage; // 如果找不到,就用传入的参数 - print(lastMsg?.toLogString()); - - // final lastMsg = controller.chatList.isNotEmpty ? controller.chatList.last : arguments.lastMessage; + logger.w(lastMsg?.toLogString()); final res = await ImService.instance.getHistoryMessageList( - userID: arguments.userID, + groupID: arguments.groupID, lastMsg: lastMsg, ); @@ -241,26 +192,21 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix if (newMessages.isEmpty) { hasMore = false; - // MyDialog.toast('没有更多了~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } if (initFlag && lastMsg != null) { newMessages.insert(0, lastMsg); - // controller.scrollToBottom(); } controller.updateChatListWithTimeLabels(newMessages); if (initFlag) { // 初始化时滚到最底部 WidgetsBinding.instance.addPostFrameCallback((_) { if (chatController.hasClients) { - // controller.scrollToBottom(); - // final bottomPadding = MediaQuery.of(context).padding.bottom; // 底部安全区域高度 - // chatController.jumpTo(chatController.position.maxScrollExtent); // 60为底部操作栏高度 chatController.jumpTo(0); } }); } - print('聊天数据加载成功'); + logger.w('群聊消息加载成功'); } else { MyDialog.toast("获取聊天记录失败:${res.desc}", icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } @@ -343,6 +289,13 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix data: item, child: Ink( child: InkWell( + onTap: () { + // 预览图片 + Get.to(() => ImageViewer( + images: [imagePaths.first], + index: 0, + )); + }, overlayColor: WidgetStateProperty.all(Colors.transparent), child: ClipRRect( borderRadius: BorderRadius.circular(10.0), @@ -525,6 +478,8 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix //price,title,url,sell final obj = jsonDecode(item.customElem!.data!); logger.e(obj); + final goodsId = obj['goodsId']; + final userID = obj['userID']; final url = obj['url']; final title = obj['title']; final price = obj['price']; @@ -532,6 +487,10 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix msgtpl.add(RenderChatItem( data: item, child: GestureDetector( + onTap: () { + // 这里带上分享人的ID + Get.toNamed('/goods', arguments: {'goodsId': goodsId, 'userID': userID}); + }, child: Container( width: 160, clipBehavior: Clip.antiAlias, @@ -593,11 +552,6 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix ], ), ), - onTap: () { - // 这里带上分享人的ID - // Get.toNamed('/goods'); - Get.toNamed('/goods', arguments: {}); - }, ), )); } @@ -606,10 +560,12 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix /// {imgUrl,videoUrl,width,height} final obj = jsonDecode(item.customElem!.data!); logger.e(obj); + final videoId = obj['videoId']; final videoUrl = obj['videoUrl']; final imgUrl = obj['imgUrl']; final width = obj['width'] as num; final height = obj['height'] as num; + final isHorizontal = width > height; msgtpl.add(RenderChatItem( data: item, child: Ink( @@ -622,9 +578,15 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix children: [ ClipRRect( borderRadius: BorderRadius.circular(10.0), - child: NetworkOrAssetImage( - imageUrl: imgUrl, + child: Container( width: 120, + height: 240, + color: Colors.black, + child: NetworkOrAssetImage( + imageUrl: imgUrl, + fit: isHorizontal ? BoxFit.contain : BoxFit.cover, + placeholderAsset: 'assets/images/bk.jpg', + ), ), ), const Align( @@ -639,31 +601,32 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix ), ), onTap: () { - showGeneralDialog( - context: context, - barrierColor: Colors.black.withAlpha((1.0 * 255).round()), - pageBuilder: (_, __, ___) { - return SafeArea( - bottom: true, - child: Padding( - padding: const EdgeInsets.only(bottom: 4), - child: PreviewVideo( - videoUrl: videoUrl, - width: width.toDouble(), - height: height.toDouble(), - ), - ), - ); - }, - transitionBuilder: (_, anim, __, child) { - return FadeTransition(opacity: anim, child: child); - }, - transitionDuration: const Duration(milliseconds: 200), - ); - }, - onLongPress: () { - contextMenuDialog(); + Get.toNamed('/videoDetail', arguments: {'videoId': videoId}); + // showGeneralDialog( + // context: context, + // barrierColor: Colors.black.withAlpha((1.0 * 255).round()), + // pageBuilder: (_, __, ___) { + // return SafeArea( + // bottom: true, + // child: Padding( + // padding: const EdgeInsets.only(bottom: 4), + // child: PreviewVideo( + // videoUrl: videoUrl, + // width: width.toDouble(), + // height: height.toDouble(), + // ), + // ), + // ); + // }, + // transitionBuilder: (_, anim, __, child) { + // return FadeTransition(opacity: anim, child: child); + // }, + // transitionDuration: const Duration(milliseconds: 200), + // ); }, + // onLongPress: () { + // contextMenuDialog(); + // }, ), ), )); @@ -1019,7 +982,7 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix } // 发送消息队列 - void sendMessage(message) async { + Future sendMessage(message) async { // 待插入的消息 List messagesToInsert = []; V2TimMessage? lastRealMsg; @@ -1054,7 +1017,7 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix late final ImResult res; res = await IMMessage().sendMessage( msg: message, - toUserID: arguments.userID, + toUserID: arguments.groupID, ); if (res.success && res.data != null) { @@ -1189,6 +1152,8 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix // 发送视频消息=5 void sendVideo(videoFilePath, type, duration, snapshotPath) async { + final instance = MyDialog.loading('处理中'); + final resImg = await IMMessage().createVideoMessage( videoFilePath: videoFilePath, type: type, @@ -1196,13 +1161,116 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix snapshotPath: snapshotPath, ); if (resImg.success) { - sendMessage(resImg.data?.messageInfo); + await sendMessage(resImg.data?.messageInfo); + instance.close(); } } + // 拍摄 + Future showPicker(BuildContext context) async { + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, // 透明底色,这样圆角才能生效 + + builder: (context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () async { + Navigator.pop(context); + await _pickPhoto(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.photo), + SizedBox(width: 8), + Text("拍摄照片"), + ], + ), + ), + ), + InkWell( + onTap: () async { + Navigator.pop(context); + await _pickVideo(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.videocam), + SizedBox(width: 8), + Text("拍摄视频"), + ], + ), + ), + ), + const Divider(height: 1), + InkWell( + onTap: () => Navigator.pop(context), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + // Icon(Icons.close), + // SizedBox(width: 8), + Text("取消"), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + /// 拍照片 + Future _pickPhoto() async { + final XFile? photo = await _picker.pickImage( + source: ImageSource.camera, + maxWidth: 1920, + maxHeight: 1080, + imageQuality: 90, + ); + if (photo != null) { + logger.w("照片路径: ${photo.path}"); + sendImage(photo.path); + } + } + + /// 拍视频 + Future _pickVideo() async { + final XFile? video = await _picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + + if (video == null) return; + logger.w("视频路径: ${video.path}"); + // 获取首帧图 + final snapshot = await generateVideoThumbnail(video.path) ?? ''; + // 获取视频时长 + final controller = VideoPlayerController.file(File(video.path)); + await controller.initialize(); + final duration = controller.value.duration.inSeconds; + await controller.dispose(); + // mimeType + final mimeType = lookupMimeType(video.path) ?? 'video/mp4'; + sendVideo(video.path, mimeType, duration, snapshot); + } + // 底部操作蓝选择区操作 void handleChooseAction(key) { - MyDialog.toast('$key'); + // MyDialog.toast('$key'); switch (key) { case 'photo': // .... @@ -1210,6 +1278,7 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix break; case 'camera': // .... + showPicker(context); break; case 'redpacket': sendRedPacketDialog(); @@ -1418,35 +1487,35 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix // 长按消息菜单 void contextMenuDialog() { - showDialog( - context: context, - builder: (context) { - return SimpleDialog( - backgroundColor: Colors.white, - surfaceTintColor: Colors.white, - contentPadding: const EdgeInsets.symmetric(vertical: 5.0), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), - children: [ - SimpleDialogOption( - child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('复制')), - onPressed: () {}, - ), - SimpleDialogOption( - child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('发送给朋友')), - onPressed: () {}, - ), - SimpleDialogOption( - child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('收藏')), - onPressed: () {}, - ), - SimpleDialogOption( - child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('删除')), - onPressed: () {}, - ), - ], - ); - }, - ); + // showDialog( + // context: context, + // builder: (context) { + // return SimpleDialog( + // backgroundColor: Colors.white, + // surfaceTintColor: Colors.white, + // contentPadding: const EdgeInsets.symmetric(vertical: 5.0), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), + // children: [ + // SimpleDialogOption( + // child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('复制')), + // onPressed: () {}, + // ), + // SimpleDialogOption( + // child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('发送给朋友')), + // onPressed: () {}, + // ), + // SimpleDialogOption( + // child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('收藏')), + // onPressed: () {}, + // ), + // SimpleDialogOption( + // child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('删除')), + // onPressed: () {}, + // ), + // ], + // ); + // }, + // ); } // 发群红包弹窗 @@ -1496,13 +1565,13 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix }, ), titleSpacing: 1.0, - title: Obx(() { - return Text( - // '${arguments['title']}', - '${arguments.showName}', - style: const TextStyle(fontSize: 18.0, fontFamily: 'Arial'), - ); - }), + title: Text( + '${arguments.showName}', + style: const TextStyle(fontSize: 18.0, fontFamily: 'Arial'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + ), flexibleSpace: Container( decoration: const BoxDecoration( gradient: LinearGradient( @@ -1603,7 +1672,6 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix switch (selected) { case 'remark': print('点击了备注'); - setRemark(); break; case 'not': print('点击了免打扰'); @@ -2189,13 +2257,13 @@ class RenderChatItem extends StatelessWidget { child: Column( crossAxisAlignment: !(data.isSelf ?? false) ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [ - // Text( - // displayName ?? '未知昵称', - // style: const TextStyle(color: Colors.grey, fontSize: 12.0), - // ), - // const SizedBox( - // height: 3.0, - // ), + Text( + displayName ?? '未知昵称', + style: const TextStyle(color: Colors.grey, fontSize: 12.0), + ), + const SizedBox( + height: 3.0, + ), Stack( children: [ // 气泡箭头 diff --git a/lib/pages/chat/index.dart b/lib/pages/chat/index.dart index bf268ef..4198f1a 100644 --- a/lib/pages/chat/index.dart +++ b/lib/pages/chat/index.dart @@ -270,15 +270,21 @@ class ChatPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/order.svg', - height: 36.0, - width: 36.0, - ), - Text('群聊'), - ], + GestureDetector( + onTap: () { + // 去群聊列表 + Get.toNamed('/groupList'); + }, + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/order.svg', + height: 36.0, + width: 36.0, + ), + Text('群聊'), + ], + ), ), GestureDetector( onTap: () { @@ -351,6 +357,7 @@ class ChatPageState extends State { return Ink( // color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色 child: InkWell( + key: ValueKey(chatList[index].conversation.conversationID), splashColor: Colors.grey[200], child: Container( padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), diff --git a/lib/pages/goods/detail.dart b/lib/pages/goods/detail.dart index bba110a..fc2e1b6 100644 --- a/lib/pages/goods/detail.dart +++ b/lib/pages/goods/detail.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/api/shop_api.dart'; @@ -33,7 +34,7 @@ class Goods extends StatefulWidget { } class _GoodsState extends State { - // late int shopId; //商品id + final shareUserId = Get.arguments['userID'] ?? ''; //分享人的id,生成订单请求时必须携带的参数 dynamic shopObj; late ScrollController scrollController = ScrollController(); final ChatController chatController = Get.find(); @@ -124,6 +125,8 @@ class _GoodsState extends State { "title": shopObj['name'], "url": shopObj['pic'], "sell": Utils.graceNumber(int.parse(shopObj['sales'] ?? '0')), + "goodsId": shopObj['id'], + "userID": Get.find().userID.value, }); final res = await IMMessage().createCustomMessage( data: makeJson, diff --git a/lib/pages/groupChat/groupList.dart b/lib/pages/groupChat/groupList.dart new file mode 100644 index 0000000..55dfd88 --- /dev/null +++ b/lib/pages/groupChat/groupList.dart @@ -0,0 +1,202 @@ +/// 已加入的群列表 +library; + +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/behavior/custom_scroll_behavior.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'; + +class Grouplist extends StatefulWidget { + const Grouplist({super.key}); + + @override + State createState() => GrouplistState(); +} + +class GrouplistState extends State with SingleTickerProviderStateMixin { + bool isLoading = false; // 是否在加载中 + bool hasMore = true; // 是否还有更多数据 + String page = ''; + List dataList = []; + + ///------------------- + + @override + void initState() { + super.initState(); + getData(); + } + + // 分页获取已加入的群聊数据 + Future getData() async { + final res = await ImService.instance.getJoinedGroupList(); + if (res.success && res.data != null) { + for (var item in res.data!) { + logger.i('获取成功:${item.toLogString()}'); + } + + final isFinished = true; + if (isFinished) { + setState(() { + hasMore = false; + }); + } + setState(() { + dataList.addAll(res.data!); + }); + } else { + logger.e('获取数据失败:${res.desc}'); + } + } + + // 下拉刷新 + Future handleRefresh() async { + dataList.clear(); + page = ''; + getData(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: const Text( + '群聊', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + actions: [], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Column( + children: [ + Expanded( + child: EasyRefresh.builder( + callLoadOverOffset: 20, //触底距离 + callRefreshOverOffset: 20, // 下拉距离 + header: ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '加载中...', + processingText: '加载中...', + processedText: '加载完成', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + footer: ClassicFooter( + dragText: '加载更多', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: hasMore ? '加载完成' : '没有更多了~', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + onRefresh: () async { + await handleRefresh(); + }, + onLoad: () async { + if (hasMore) { + await getData(); + } + }, + childBuilder: (context, physics) { + return ListView.builder( + physics: physics, + itemCount: dataList.length, + itemBuilder: (context, index) { + final item = dataList[index]; + return Ink( + key: ValueKey(item.groupID), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧部分(头像 + 群名称 + 群介绍) + Expanded( + child: InkWell( + onTap: () async { + // 这里点击跳转去群聊,先获取会话 + final res = await ImService.instance.getConversation(conversationID: 'group_${item.groupID}'); + if (res.success) { + V2TimConversation conversation = res.data; + logger.w(conversation.toLogString()); + conversation.showName = conversation.showName ?? item.groupName; + Get.toNamed( + '/chatGroup', + arguments: conversation, + ); + } + }, + child: Row( + children: [ + ClipOval( + child: NetworkOrAssetImage( + imageUrl: item.faceUrl, + width: 50, + height: 50, + placeholderAsset: 'assets/images/group.png', + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.groupName?.isNotEmpty == true ? item.groupName! : '群聊', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + if (item.introduction?.isNotEmpty ?? false) ...[ + const SizedBox(height: 2.0), + Text( + item.introduction!, + style: const TextStyle( + color: Colors.grey, + fontSize: 13.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/index/index.dart b/lib/pages/index/index.dart index 20df39d..9cfe351 100644 --- a/lib/pages/index/index.dart +++ b/lib/pages/index/index.dart @@ -2,14 +2,17 @@ library; import 'package:card_swiper/card_swiper.dart'; +import 'package:dynamic_tabbar/dynamic_tabbar.dart'; import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:get/get.dart'; +import 'package:loopin/IM/im_friend_listeners.dart'; import 'package:loopin/components/backtop.dart'; import 'package:loopin/components/loading.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/controller/shop_index_controller.dart'; +import 'package:loopin/styles/index.dart'; import 'package:loopin/utils/index.dart'; class IndexPage extends StatefulWidget { @@ -127,13 +130,62 @@ class _IndexPageState extends State with SingleTickerProviderStateMix // 内容区域 Expanded( child: controller.tabController == null - ? const Center(child: CircularProgressIndicator()) - : TabBarView( - controller: controller.tabController, - children: List.generate( - controller.tabList.length, - (index) => _buildTabContent(index), - ), + ? Center(child: CircularProgressIndicator()) + : Obx( + () { + final tabs = controller.tabList.asMap().entries.map((entry) { + final idx = entry.key; + final item = entry.value; + return TabData( + index: idx, + title: Tab( + child: Center( + child: Text( + item['name'] ?? '', + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ), + ), + content: _buildTabContent(idx), + ); + }).toList(); + return DynamicTabBarWidget( + onTabControllerUpdated: (tabController) { + controller.tabController = tabController; + // 强制选中第一个 + if (tabController.index != 0) { + tabController.animateTo(0); + } + }, + onTabChanged: (index) { + logger.w("当前选中tab: $index"); + }, + dynamicTabs: tabs, + tabAlignment: TabAlignment.start, + isScrollable: true, + showNextIcon: false, // 显示前进图标 + showBackIcon: false, // 显示后退图标 + labelColor: FStyle.primaryColor, + indicatorColor: FStyle.primaryColor, + overlayColor: WidgetStateProperty.all(Colors.transparent), + unselectedLabelColor: Colors.black87, + indicator: const UnderlineTabIndicator( + borderSide: BorderSide(color: Color.fromARGB(255, 236, 108, 49), width: 2.0), + ), + unselectedLabelStyle: const TextStyle( + fontSize: 14.0, + fontFamily: 'Microsoft YaHei', + ), + labelStyle: const TextStyle( + fontSize: 14.0, + fontFamily: 'Microsoft YaHei', + fontWeight: FontWeight.bold, + ), + dividerHeight: 0, + ); + }, ), ), ], @@ -156,7 +208,6 @@ class _IndexPageState extends State with SingleTickerProviderStateMix if (controller.tabController == null) { return SizedBox(); } - return Column( children: [ // 轮播图 @@ -186,48 +237,6 @@ class _IndexPageState extends State with SingleTickerProviderStateMix ), ); }), - - // TabBar - Obx(() { - int tabCount = controller.tabList.length; - double screenWidth = MediaQuery.of(context).size.width; - double minTabWidth = 60; - bool isScrollable = tabCount * minTabWidth > screenWidth; - - return Container( - color: Colors.white, - child: TabBar( - controller: controller.tabController, - tabs: controller.tabList.map((item) { - return Tab( - child: Text( - item['name'] ?? '', - style: const TextStyle(fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - softWrap: false, - ), - ); - }).toList(), - isScrollable: isScrollable, - overlayColor: WidgetStateProperty.all(Colors.transparent), - unselectedLabelColor: Colors.black87, - labelColor: const Color.fromARGB(255, 236, 108, 49), - indicator: const UnderlineTabIndicator( - borderSide: BorderSide(color: Color.fromARGB(255, 236, 108, 49), width: 2.0), - ), - unselectedLabelStyle: const TextStyle( - fontSize: 14.0, - fontFamily: 'Microsoft YaHei', - ), - labelStyle: const TextStyle( - fontSize: 16.0, - fontFamily: 'Microsoft YaHei', - fontWeight: FontWeight.bold, - ), - dividerHeight: 0, - ), - ); - }), ], ); } diff --git a/lib/pages/my/index.dart b/lib/pages/my/index.dart index 027d5d3..825db97 100644 --- a/lib/pages/my/index.dart +++ b/lib/pages/my/index.dart @@ -554,6 +554,7 @@ class MyPageState extends State with SingleTickerProviderStateMixin { return InkWell( onTap: () { //去视频详情 + Get.toNamed('/videoDetail', arguments: {'videoId': item['id']}); }, onLongPress: () { showModalBottomSheet( diff --git a/lib/pages/video/commonVideo.dart b/lib/pages/video/commonVideo.dart index 86ede32..ad2337c 100644 --- a/lib/pages/video/commonVideo.dart +++ b/lib/pages/video/commonVideo.dart @@ -6,18 +6,19 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_core.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_service.dart' hide logger; import 'package:loopin/api/video_api.dart'; import 'package:loopin/components/my_toast.dart'; import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/models/share_type.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/utils/download_video.dart'; import 'package:loopin/utils/permissions.dart'; import 'package:loopin/utils/wxsdk.dart'; -import 'package:loopin/models/share_type.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart'; @@ -497,15 +498,19 @@ class _VideoDetailPageState extends State { void handlCoverClick(V2TimConversation conv) async { // 发送VideoMsg,获取当前视频信息 final userId = conv.userID; + logger.w(videoData); final String url = videoData['url']; - final img = videoData['firstFrameImg']; + final img = (videoData['cover'] != null && videoData['cover'].toString().isNotEmpty) ? videoData['cover'] : videoData['firstFrameImg']; final width = videoData['width']; final height = videoData['height']; + final videoId = videoData['id']; final makeJson = jsonEncode({ "width": width, "height": height, "imgUrl": img, "videoUrl": url, + "videoId": videoId, + "userID": Get.find().userID.value, }); final res = await IMMessage().createCustomMessage( data: makeJson, @@ -570,343 +575,346 @@ class _VideoDetailPageState extends State { onPressed: () => Navigator.pop(context), ), ), - body: Stack( - children: [ - // 视频区域 - Positioned.fill( - child: GestureDetector( - child: Stack( - children: [ - Visibility( - visible: position > Duration.zero, - child: Video( - controller: videoController, - fit: isHorizontal ? BoxFit.contain : BoxFit.cover, - controls: NoVideoControls, + body: SafeArea( + bottom: true, + child: Stack( + children: [ + // 视频区域 + Positioned.fill( + child: GestureDetector( + child: Stack( + children: [ + Visibility( + visible: position > Duration.zero, + child: Video( + controller: videoController, + fit: isHorizontal ? BoxFit.contain : BoxFit.cover, + controls: NoVideoControls, + ), ), - ), - AnimatedOpacity( - opacity: position > Duration(milliseconds: 100) ? 0.0 : 1.0, - duration: Duration(milliseconds: 50), - child: Image.network( - videoData['firstFrameImg'] ?? 'https://wuzhongjie.com.cn/download/logo.png', - fit: isHorizontal ? BoxFit.contain : BoxFit.cover, - width: double.infinity, - height: double.infinity, + AnimatedOpacity( + opacity: position > Duration(milliseconds: 100) ? 0.0 : 1.0, + duration: Duration(milliseconds: 50), + child: Image.network( + videoData['firstFrameImg'] ?? 'https://wuzhongjie.com.cn/download/logo.png', + fit: isHorizontal ? BoxFit.contain : BoxFit.cover, + width: double.infinity, + height: double.infinity, + ), ), - ), - StreamBuilder( - stream: player.stream.playing, - builder: (context, playing) { - return Visibility( - visible: playing.data == false, - child: Center( - child: IconButton( - padding: EdgeInsets.zero, - onPressed: () { - player.playOrPause(); - }, - icon: Icon( - playing.data == true ? Icons.pause : Icons.play_arrow_rounded, - color: Colors.white60, - size: 80, + StreamBuilder( + stream: player.stream.playing, + builder: (context, playing) { + return Visibility( + visible: playing.data == false, + child: Center( + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () { + player.playOrPause(); + }, + icon: Icon( + playing.data == true ? Icons.pause : Icons.play_arrow_rounded, + color: Colors.white60, + size: 80, + ), + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.black.withAlpha(15))), + ), + ), + ); + }, + ), + ], + ), + onTap: () { + player.playOrPause(); + }, + ), + ), + + // 右侧操作栏 + Positioned( + bottom: 100.0, + right: 6.0, + child: Column( + spacing: 15.0, + children: [ + Stack( + children: [ + SizedBox( + height: 55.0, + width: 48.0, + child: GestureDetector( + onTap: () async { + player.pause(); + // 跳转到 Vloger 页面并等待返回结果 + final result = await Get.toNamed('/vloger', arguments: videoData); + if (result != null) { + // 处理返回的参数 + player.play(); + setState(() { + videoData['doIFollowVloger'] = result['followStatus']; + }); + } + }, + child: UnconstrainedBox( + alignment: Alignment.topCenter, + child: Container( + height: 48.0, + width: 48.0, + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2.0), + borderRadius: BorderRadius.circular(100.0), + ), + child: ClipOval( + child: NetworkOrAssetImage( + imageUrl: videoData['vlogerFace'] ?? videoData['commentUserFace'], + ), + ), ), - style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.black.withAlpha(15))), ), ), + ), + Positioned( + bottom: 0, + left: 15.0, + child: InkWell( + child: Container( + height: 18.0, + width: 18.0, + decoration: BoxDecoration( + color: videoData['doIFollowVloger'] == true ? Colors.white : Color(0xFFFF5000), + borderRadius: BorderRadius.circular(100.0), + ), + child: Icon( + videoData['doIFollowVloger'] == true ? Icons.check : Icons.add, + color: videoData['doIFollowVloger'] == true ? Color(0xFFFF5000) : Colors.white, + size: 14.0, + ), + ), + onTap: () async { + if (videoData['doIFollowVloger'] == true) { + await unfollowUser(); + } else { + await followUser(); + } + }, + ), + ), + ], + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/heart.svg', + colorFilter: ColorFilter.mode(videoData['doILikeThisVlog'] == true ? Color(0xFFFF5000) : Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + Text( + '${videoData['likeCounts'] ?? 0}', + style: TextStyle(color: Colors.white, fontSize: 12.0), + ), + ], + ), + onTap: () { + if (videoData['doILikeThisVlog'] == true) { + doUnLikeVideo(); + } else { + doLikeVideo(); + } + }, + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/reply.svg', + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + Text( + '${videoData['commentsCounts'] ?? 0}', + style: TextStyle(color: Colors.white, fontSize: 12.0), + ), + ], + ), + onTap: () { + handleComment(); + }, + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/share.svg', + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + ], + ), + onTap: () { + handleShare(); + }, + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/report.svg', + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + ], + ), + onTap: () async { + player.pause(); + // 跳转到举报页面并等待返回结果 + final result = await Get.toNamed('/report', arguments: videoData); + if (result != null) { + player.play(); + } + }, + ), + ], + ), + ), + + // 底部信息区域 + Positioned( + bottom: 15.0, + left: 10.0, + right: 80.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '@${videoData['vlogerNickname'] ?? videoData['commentUserNickname'] ?? '未知用户'}', + style: const TextStyle(color: Colors.white, fontSize: 16.0), + ), + LayoutBuilder( + builder: (context, constraints) { + final text = videoData['title'] ?? '未知'; + final span = TextSpan( + text: text, + style: const TextStyle(color: Colors.white, fontSize: 14.0), + ); + final tp = TextPainter( + text: span, + maxLines: 3, + textDirection: TextDirection.ltr, + ); + tp.layout(maxWidth: constraints.maxWidth); + final isOverflow = tp.didExceedMaxLines; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + maxLines: videoData['expanded'] ?? false ? null : 3, + overflow: videoData['expanded'] ?? false ? TextOverflow.visible : TextOverflow.ellipsis, + style: const TextStyle(color: Colors.white, fontSize: 14.0), + ), + if (isOverflow) + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: GestureDetector( + onTap: () { + setState(() { + videoData['expanded'] = !(videoData['expanded'] ?? false); + }); + }, + child: Text( + videoData['expanded'] ?? false ? '收起' : '展开更多', + textAlign: TextAlign.right, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], ); }, ), ], ), - onTap: () { - player.playOrPause(); - }, ), - ), - // 右侧操作栏 - Positioned( - bottom: 100.0, - right: 6.0, - child: Column( - spacing: 15.0, - children: [ - Stack( - children: [ - SizedBox( - height: 55.0, - width: 48.0, - child: GestureDetector( - onTap: () async { - player.pause(); - // 跳转到 Vloger 页面并等待返回结果 - final result = await Get.toNamed('/vloger', arguments: videoData); - if (result != null) { - // 处理返回的参数 - player.play(); - setState(() { - videoData['doIFollowVloger'] = result['followStatus']; - }); - } - }, - child: UnconstrainedBox( - alignment: Alignment.topCenter, - child: Container( - height: 48.0, - width: 48.0, - decoration: BoxDecoration( - border: Border.all(color: Colors.white, width: 2.0), - borderRadius: BorderRadius.circular(100.0), - ), - child: ClipOval( - child: NetworkOrAssetImage( - imageUrl: videoData['vlogerFace'] ?? videoData['commentUserFace'], - ), - ), - ), - ), - ), + // 进度条 + Positioned( + bottom: 0.0, + left: 6.0, + right: 6.0, + child: Visibility( + visible: position > Duration.zero, + child: Listener( + child: SliderTheme( + data: SliderThemeData( + trackHeight: sliderDraging ? 6.0 : 2.0, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0), + overlayShape: RoundSliderOverlayShape(overlayRadius: 0), + inactiveTrackColor: Colors.white24, + activeTrackColor: Colors.white, + thumbColor: Colors.white, + overlayColor: Colors.transparent, ), - Positioned( - bottom: 0, - left: 15.0, - child: InkWell( - child: Container( - height: 18.0, - width: 18.0, - decoration: BoxDecoration( - color: videoData['doIFollowVloger'] == true ? Colors.white : Color(0xFFFF5000), - borderRadius: BorderRadius.circular(100.0), - ), - child: Icon( - videoData['doIFollowVloger'] == true ? Icons.check : Icons.add, - color: videoData['doIFollowVloger'] == true ? Color(0xFFFF5000) : Colors.white, - size: 14.0, - ), - ), - onTap: () async { - if (videoData['doIFollowVloger'] == true) { - await unfollowUser(); - } else { - await followUser(); - } - }, - ), + child: Slider( + value: sliderValue, + onChanged: (value) async { + setState(() { + sliderValue = value; + }); + await player.seek(duration * value.clamp(0.0, 1.0)); + }, + onChangeEnd: (value) async { + setState(() { + sliderDraging = false; + }); + if (!player.state.playing) { + await player.play(); + } + }, ), - ], - ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/heart.svg', - colorFilter: ColorFilter.mode(videoData['doILikeThisVlog'] == true ? Color(0xFFFF5000) : Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - Text( - '${videoData['likeCounts'] ?? 0}', - style: TextStyle(color: Colors.white, fontSize: 12.0), - ), - ], ), - onTap: () { - if (videoData['doILikeThisVlog'] == true) { - doUnLikeVideo(); - } else { - doLikeVideo(); - } + onPointerMove: (e) { + setState(() { + sliderDraging = true; + }); }, ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/reply.svg', - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - Text( - '${videoData['commentsCounts'] ?? 0}', - style: TextStyle(color: Colors.white, fontSize: 12.0), - ), - ], - ), - onTap: () { - handleComment(); - }, - ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/share.svg', - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - ], - ), - onTap: () { - handleShare(); - }, - ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/report.svg', - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - ], - ), - onTap: () async { - player.pause(); - // 跳转到举报页面并等待返回结果 - final result = await Get.toNamed('/report', arguments: videoData); - if (result != null) { - player.play(); - } - }, - ), - ], - ), - ), - - // 底部信息区域 - Positioned( - bottom: 15.0, - left: 10.0, - right: 80.0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '@${videoData['vlogerNickname'] ?? videoData['commentUserNickname'] ?? '未知用户'}', - style: const TextStyle(color: Colors.white, fontSize: 16.0), - ), - LayoutBuilder( - builder: (context, constraints) { - final text = videoData['title'] ?? '未知'; - final span = TextSpan( - text: text, - style: const TextStyle(color: Colors.white, fontSize: 14.0), - ); - final tp = TextPainter( - text: span, - maxLines: 3, - textDirection: TextDirection.ltr, - ); - tp.layout(maxWidth: constraints.maxWidth); - final isOverflow = tp.didExceedMaxLines; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - text, - maxLines: videoData['expanded'] ?? false ? null : 3, - overflow: videoData['expanded'] ?? false ? TextOverflow.visible : TextOverflow.ellipsis, - style: const TextStyle(color: Colors.white, fontSize: 14.0), - ), - if (isOverflow) - Padding( - padding: const EdgeInsets.only(top: 6.0), - child: GestureDetector( - onTap: () { - setState(() { - videoData['expanded'] = !(videoData['expanded'] ?? false); - }); - }, - child: Text( - videoData['expanded'] ?? false ? '收起' : '展开更多', - textAlign: TextAlign.right, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ); - }, - ), - ], - ), - ), - - // 进度条 - Positioned( - bottom: 0.0, - left: 6.0, - right: 6.0, - child: Visibility( - visible: position > Duration.zero, - child: Listener( - child: SliderTheme( - data: SliderThemeData( - trackHeight: sliderDraging ? 6.0 : 2.0, - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0), - overlayShape: RoundSliderOverlayShape(overlayRadius: 0), - inactiveTrackColor: Colors.white24, - activeTrackColor: Colors.white, - thumbColor: Colors.white, - overlayColor: Colors.transparent, - ), - child: Slider( - value: sliderValue, - onChanged: (value) async { - setState(() { - sliderValue = value; - }); - await player.seek(duration * value.clamp(0.0, 1.0)); - }, - onChangeEnd: (value) async { - setState(() { - sliderDraging = false; - }); - if (!player.state.playing) { - await player.play(); - } - }, - ), - ), - onPointerMove: (e) { - setState(() { - sliderDraging = true; - }); - }, ), ), - ), - // 时间显示 - Positioned( - bottom: 100.0, - left: 10.0, - right: 10.0, - child: Visibility( - visible: sliderDraging, - child: DefaultTextStyle( - style: TextStyle(color: Colors.white54, fontSize: 18.0, fontFamily: 'Arial'), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8.0, - children: [ - Text(position.label(reference: duration), style: TextStyle(color: Colors.white)), - Text('/', style: TextStyle(fontSize: 14.0)), - Text(duration.label(reference: duration)), - ], - ), - )), - ), - ], + // 时间显示 + Positioned( + bottom: 100.0, + left: 10.0, + right: 10.0, + child: Visibility( + visible: sliderDraging, + child: DefaultTextStyle( + style: TextStyle(color: Colors.white54, fontSize: 18.0, fontFamily: 'Arial'), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8.0, + children: [ + Text(position.label(reference: duration), style: TextStyle(color: Colors.white)), + Text('/', style: TextStyle(fontSize: 14.0)), + Text(duration.label(reference: duration)), + ], + ), + )), + ), + ], + ), ), ); } diff --git a/lib/pages/video/module/friend.dart b/lib/pages/video/module/friend.dart index 6a390fd..da71114 100644 --- a/lib/pages/video/module/friend.dart +++ b/lib/pages/video/module/friend.dart @@ -9,18 +9,19 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_core.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_service.dart' hide logger; import 'package:loopin/api/video_api.dart'; import 'package:loopin/components/my_toast.dart'; import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/models/share_type.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/utils/download_video.dart'; import 'package:loopin/utils/permissions.dart'; import 'package:loopin/utils/wxsdk.dart'; -import 'package:loopin/models/share_type.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart'; @@ -982,15 +983,20 @@ class _FriendModuleState extends State { void handlCoverClick(V2TimConversation conv) async { // 发送VideoMsg,获取当前视频信息 final userId = conv.userID; - final String url = videoList[videoModuleController.videoPlayIndex.value]['url']; - final img = videoList[videoModuleController.videoPlayIndex.value]['firstFrameImg']; - final width = videoList[videoModuleController.videoPlayIndex.value]['width']; - final height = videoList[videoModuleController.videoPlayIndex.value]['height']; + final currentVideo = videoList[videoModuleController.videoPlayIndex.value]; + logger.w(currentVideo); + final img = (currentVideo['cover'] != null && currentVideo['cover'].toString().isNotEmpty) ? currentVideo['cover'] : currentVideo['firstFrameImg']; + final url = currentVideo['url']; + final width = currentVideo['width']; + final height = currentVideo['height']; + final videoId = currentVideo['id']; final makeJson = jsonEncode({ "width": width, "height": height, "imgUrl": img, "videoUrl": url, + "videoId": videoId, + "userID": Get.find().userID.value, }); final res = await IMMessage().createCustomMessage( data: makeJson, @@ -1019,399 +1025,397 @@ class _FriendModuleState extends State { child: Column( children: [ // 添加暂无数据提示 - if (videoList.isEmpty && !isLoadingMore) - Expanded( - child: Center( - child: Text( - '暂无数据', - style: TextStyle( - color: Colors.white, - fontSize: 16.0, + if (videoList.isEmpty && !isLoadingMore) + Expanded( + child: Center( + child: Text( + '暂无数据', + style: TextStyle( + color: Colors.white, + fontSize: 16.0, + ), ), ), - ), - ) - else - Expanded( - child: Stack( - children: [ - /// 垂直滚动模块 - PageView.builder( - scrollBehavior: CustomScrollBehavior().copyWith(scrollbars: false), - scrollDirection: Axis.vertical, - controller: pageController, - onPageChanged: (index) async { - videoModuleController.updateVideoPlayIndex(index); - setState(() { - sliderValue = 0.0; - sliderDraging = false; - position = Duration.zero; - duration = Duration.zero; - }); + ) + else + Expanded( + child: Stack( + children: [ + /// 垂直滚动模块 + PageView.builder( + scrollBehavior: CustomScrollBehavior().copyWith(scrollbars: false), + scrollDirection: Axis.vertical, + controller: pageController, + onPageChanged: (index) async { + videoModuleController.updateVideoPlayIndex(index); + setState(() { + sliderValue = 0.0; + sliderDraging = false; + position = Duration.zero; + duration = Duration.zero; + }); - player.stop(); - await player.open(Media(videoList[index]['url'])); - if (index == videoList.length - 2 && !isLoadingMore) { - await fetchVideoList(); - } - }, - itemCount: videoList.length, - itemBuilder: (context, index) { - final videoWidth = videoList[index]['width'] ?? 1; - final videoHeight = videoList[index]['height'] ?? 1; - final isHorizontal = videoWidth > videoHeight; - final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList(); - return Stack( - children: [ - // 视频区域 - Positioned( - top: 0, - left: 0, - right: 0, - bottom: 0, - child: GestureDetector( - child: Stack( - children: [ - Visibility( - visible: videoModuleController.videoPlayIndex.value == index && position > Duration.zero, - child: Video( - controller: videoController, - fit: isHorizontal ? BoxFit.contain : BoxFit.cover, - controls: NoVideoControls, - ), - ), - AnimatedOpacity( - opacity: videoModuleController.videoPlayIndex.value == index && position > Duration(milliseconds: 100) ? 0.0 : 1.0, - duration: Duration(milliseconds: 50), - child: Image.network( - videoList[index]['firstFrameImg'] ?? 'https://wuzhongjie.com.cn/download/logo.png', - fit: isHorizontal ? BoxFit.contain : BoxFit.cover, - width: double.infinity, - height: double.infinity, - ), - ), - StreamBuilder( - stream: player.stream.playing, - builder: (context, playing) { - return Visibility( - visible: playing.data == false, - child: Center( - child: IconButton( - padding: EdgeInsets.zero, - onPressed: () { - player.playOrPause(); - }, - icon: Icon( - playing.data == true ? Icons.pause : Icons.play_arrow_rounded, - color: Colors.white60, - size: 80, - ), - style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.black.withAlpha(15))), - ), - ), - ); - }, - ), - ], - ), - onTap: () { - player.playOrPause(); - }, - ), - ), - // 右侧操作栏 - Positioned( - bottom: 100.0, - right: 6.0, - child: Column( - spacing: 15.0, - children: [ - Stack( + player.stop(); + await player.open(Media(videoList[index]['url'])); + if (index == videoList.length - 2 && !isLoadingMore) { + await fetchVideoList(); + } + }, + itemCount: videoList.length, + itemBuilder: (context, index) { + final videoWidth = videoList[index]['width'] ?? 1; + final videoHeight = videoList[index]['height'] ?? 1; + final isHorizontal = videoWidth > videoHeight; + final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList(); + return Stack( + children: [ + // 视频区域 + Positioned( + top: 0, + left: 0, + right: 0, + bottom: 0, + child: GestureDetector( + child: Stack( children: [ - SizedBox( - height: 55.0, - width: 48.0, - child: GestureDetector( - onTap: () async { - player.pause(); - // 跳转到 Vloger 页面并等待返回结果 - final result = await Get.toNamed('/vloger', arguments: videoList[videoModuleController.videoPlayIndex.value]); - if (result != null) { - // 处理返回的参数 - print('返回的数据: ${result['followStatus']}'); - player.play(); - videoList[index]['doIFollowVloger'] = result['followStatus']; - } - }, - child: UnconstrainedBox( - alignment: Alignment.topCenter, - child: Container( - height: 48.0, - width: 48.0, - decoration: BoxDecoration( - border: Border.all(color: Colors.white, width: 2.0), - borderRadius: BorderRadius.circular(100.0), - ), - child: ClipOval( - child: NetworkOrAssetImage( - imageUrl: videoList[index]['commentUserFace'], - ), - ), - ), - ), + Visibility( + visible: videoModuleController.videoPlayIndex.value == index && position > Duration.zero, + child: Video( + controller: videoController, + fit: isHorizontal ? BoxFit.contain : BoxFit.cover, + controls: NoVideoControls, ), ), - Positioned( - bottom: 0, - left: 15.0, - child: InkWell( - child: Container( - height: 18.0, - width: 18.0, - decoration: BoxDecoration( - color: videoList[index]['doIFollowVloger'] ? Colors.white : Color(0xFFFF5000), - borderRadius: BorderRadius.circular(100.0), - ), - child: Icon( - videoList[index]['doIFollowVloger'] ? Icons.check : Icons.add, - color: videoList[index]['doIFollowVloger'] ? Color(0xFFFF5000) : Colors.white, - size: 14.0, - ), - ), - onTap: () async { - final vlogerId = videoList[index]['memberId']; - final doIFollowVloger = videoList[index]['doIFollowVloger']; - // 未关注点击才去关注 - if (doIFollowVloger == false) { - final res = await ImService.instance.followUser(userIDList: [vlogerId]); - if (res.success) { - setState(() { - videoList[index]['doIFollowVloger'] = !videoList[index]['doIFollowVloger']; - }); - } - } - }, + AnimatedOpacity( + opacity: videoModuleController.videoPlayIndex.value == index && position > Duration(milliseconds: 100) ? 0.0 : 1.0, + duration: Duration(milliseconds: 50), + child: Image.network( + videoList[index]['firstFrameImg'] ?? 'https://wuzhongjie.com.cn/download/logo.png', + fit: isHorizontal ? BoxFit.contain : BoxFit.cover, + width: double.infinity, + height: double.infinity, ), ), + StreamBuilder( + stream: player.stream.playing, + builder: (context, playing) { + return Visibility( + visible: playing.data == false, + child: Center( + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () { + player.playOrPause(); + }, + icon: Icon( + playing.data == true ? Icons.pause : Icons.play_arrow_rounded, + color: Colors.white60, + size: 80, + ), + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.black.withAlpha(15))), + ), + ), + ); + }, + ), ], ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/heart.svg', - colorFilter: ColorFilter.mode(videoList[index]['doILikeThisVlog'] ? Color(0xFFFF5000) : Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - Text( - '${videoList[index]['likeCounts'] + (videoList[index]['doILikeThisVlog'] ? 1 : 0)}', - style: TextStyle(color: Colors.white, fontSize: 12.0), - ), - ], - ), - onTap: () { - logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); - if (videoList[index]['doILikeThisVlog'] == true) { - logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); - doUnLikeVideo(videoList[index]); - } else { - doLikeVideo(videoList[index]); - } - }, - ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/reply.svg', - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - Text( - '${videoList[index]['commentsCounts']}', - style: TextStyle(color: Colors.white, fontSize: 12.0), - ), - ], - ), - onTap: () { - handleComment(index); - }, - ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/share.svg', - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - ], - ), - onTap: () { - handleShare(index); - }, - ), - GestureDetector( - child: Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/report.svg', - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - height: 40.0, - width: 40.0, - ), - ], - ), - onTap: ()async { - player.pause(); - // 跳转到举报页面并等待返回结果 - final result = await Get.toNamed( - '/report', - arguments: videoList[videoModuleController - .videoPlayIndex.value]); - if (result != null) { - player.play(); - }; - }, - ), - ], - ), - ), - Positioned( - bottom: 15.0, - left: 10.0, - right: 80.0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '@${videoList[videoModuleController.videoPlayIndex.value]['commentUserNickname'] ?? '未知'}', - style: const TextStyle(color: Colors.white, fontSize: 16.0), - ), - LayoutBuilder( - builder: (context, constraints) { - final text = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '未知'; - final span = TextSpan( - text: text, - style: const TextStyle(color: Colors.white, fontSize: 14.0), - ); - final tp = TextPainter( - text: span, - maxLines: 3, - textDirection: TextDirection.ltr, - ); - tp.layout(maxWidth: constraints.maxWidth); - final isOverflow = tp.didExceedMaxLines; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - text, - maxLines: videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? null : 3, - overflow: - videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? TextOverflow.visible : TextOverflow.ellipsis, - style: const TextStyle(color: Colors.white, fontSize: 14.0), - ), - if (isOverflow) - Padding( - padding: const EdgeInsets.only(top: 6.0), - child: GestureDetector( - onTap: () { - setState(() { - videoList[videoModuleController.videoPlayIndex.value]['expanded'] = - !videoList[videoModuleController.videoPlayIndex.value]['expanded']; - }); - }, - child: Text( - videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? '收起' : '展开更多', - textAlign: TextAlign.right, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ); - }, - ), - ], - )), - Positioned( - bottom: 0.0, - left: 6.0, - right: 6.0, - child: Visibility( - visible: videoModuleController.videoPlayIndex.value == index && position > Duration.zero, - child: Listener( - child: SliderTheme( - data: SliderThemeData( - trackHeight: sliderDraging ? 6.0 : 2.0, - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0), - overlayShape: RoundSliderOverlayShape(overlayRadius: 0), - inactiveTrackColor: Colors.white24, - activeTrackColor: Colors.white, - thumbColor: Colors.white, - overlayColor: Colors.transparent, - ), - child: Slider( - value: sliderValue, - onChanged: (value) async { - setState(() { - sliderValue = value; - }); - await player.seek(duration * value.clamp(0.0, 1.0)); - }, - onChangeEnd: (value) async { - setState(() { - sliderDraging = false; - }); - if (!player.state.playing) { - await player.play(); - } - }, - ), - ), - onPointerMove: (e) { - setState(() { - sliderDraging = true; - }); + onTap: () { + player.playOrPause(); }, ), ), - ), - Positioned( - bottom: 100.0, - left: 10.0, - right: 10.0, - child: Visibility( - visible: sliderDraging, - child: DefaultTextStyle( - style: TextStyle(color: Colors.white54, fontSize: 18.0, fontFamily: 'Arial'), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8.0, + // 右侧操作栏 + Positioned( + bottom: 100.0, + right: 6.0, + child: Column( + spacing: 15.0, + children: [ + Stack( children: [ - Text(position.label(reference: duration), style: TextStyle(color: Colors.white)), - Text('/', style: TextStyle(fontSize: 14.0)), - Text(duration.label(reference: duration)), + SizedBox( + height: 55.0, + width: 48.0, + child: GestureDetector( + onTap: () async { + player.pause(); + // 跳转到 Vloger 页面并等待返回结果 + final result = await Get.toNamed('/vloger', arguments: videoList[videoModuleController.videoPlayIndex.value]); + if (result != null) { + // 处理返回的参数 + print('返回的数据: ${result['followStatus']}'); + player.play(); + videoList[index]['doIFollowVloger'] = result['followStatus']; + } + }, + child: UnconstrainedBox( + alignment: Alignment.topCenter, + child: Container( + height: 48.0, + width: 48.0, + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2.0), + borderRadius: BorderRadius.circular(100.0), + ), + child: ClipOval( + child: NetworkOrAssetImage( + imageUrl: videoList[index]['commentUserFace'], + ), + ), + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 15.0, + child: InkWell( + child: Container( + height: 18.0, + width: 18.0, + decoration: BoxDecoration( + color: videoList[index]['doIFollowVloger'] ? Colors.white : Color(0xFFFF5000), + borderRadius: BorderRadius.circular(100.0), + ), + child: Icon( + videoList[index]['doIFollowVloger'] ? Icons.check : Icons.add, + color: videoList[index]['doIFollowVloger'] ? Color(0xFFFF5000) : Colors.white, + size: 14.0, + ), + ), + onTap: () async { + final vlogerId = videoList[index]['memberId']; + final doIFollowVloger = videoList[index]['doIFollowVloger']; + // 未关注点击才去关注 + if (doIFollowVloger == false) { + final res = await ImService.instance.followUser(userIDList: [vlogerId]); + if (res.success) { + setState(() { + videoList[index]['doIFollowVloger'] = !videoList[index]['doIFollowVloger']; + }); + } + } + }, + ), + ), ], ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/heart.svg', + colorFilter: ColorFilter.mode(videoList[index]['doILikeThisVlog'] ? Color(0xFFFF5000) : Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + Text( + '${videoList[index]['likeCounts'] + (videoList[index]['doILikeThisVlog'] ? 1 : 0)}', + style: TextStyle(color: Colors.white, fontSize: 12.0), + ), + ], + ), + onTap: () { + logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); + if (videoList[index]['doILikeThisVlog'] == true) { + logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); + doUnLikeVideo(videoList[index]); + } else { + doLikeVideo(videoList[index]); + } + }, + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/reply.svg', + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + Text( + '${videoList[index]['commentsCounts']}', + style: TextStyle(color: Colors.white, fontSize: 12.0), + ), + ], + ), + onTap: () { + handleComment(index); + }, + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/share.svg', + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + ], + ), + onTap: () { + handleShare(index); + }, + ), + GestureDetector( + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/report.svg', + colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), + height: 40.0, + width: 40.0, + ), + ], + ), + onTap: () async { + player.pause(); + // 跳转到举报页面并等待返回结果 + final result = await Get.toNamed('/report', arguments: videoList[videoModuleController.videoPlayIndex.value]); + if (result != null) { + player.play(); + } + }, + ), + ], + ), + ), + Positioned( + bottom: 15.0, + left: 10.0, + right: 80.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '@${videoList[videoModuleController.videoPlayIndex.value]['commentUserNickname'] ?? '未知'}', + style: const TextStyle(color: Colors.white, fontSize: 16.0), + ), + LayoutBuilder( + builder: (context, constraints) { + final text = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '未知'; + final span = TextSpan( + text: text, + style: const TextStyle(color: Colors.white, fontSize: 14.0), + ); + final tp = TextPainter( + text: span, + maxLines: 3, + textDirection: TextDirection.ltr, + ); + tp.layout(maxWidth: constraints.maxWidth); + final isOverflow = tp.didExceedMaxLines; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + maxLines: videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? null : 3, + overflow: videoList[videoModuleController.videoPlayIndex.value]['expanded'] + ? TextOverflow.visible + : TextOverflow.ellipsis, + style: const TextStyle(color: Colors.white, fontSize: 14.0), + ), + if (isOverflow) + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: GestureDetector( + onTap: () { + setState(() { + videoList[videoModuleController.videoPlayIndex.value]['expanded'] = + !videoList[videoModuleController.videoPlayIndex.value]['expanded']; + }); + }, + child: Text( + videoList[videoModuleController.videoPlayIndex.value]['expanded'] ? '收起' : '展开更多', + textAlign: TextAlign.right, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ); + }, + ), + ], )), - ), - ], - ); - }, - ), - ], + Positioned( + bottom: 0.0, + left: 6.0, + right: 6.0, + child: Visibility( + visible: videoModuleController.videoPlayIndex.value == index && position > Duration.zero, + child: Listener( + child: SliderTheme( + data: SliderThemeData( + trackHeight: sliderDraging ? 6.0 : 2.0, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0), + overlayShape: RoundSliderOverlayShape(overlayRadius: 0), + inactiveTrackColor: Colors.white24, + activeTrackColor: Colors.white, + thumbColor: Colors.white, + overlayColor: Colors.transparent, + ), + child: Slider( + value: sliderValue, + onChanged: (value) async { + setState(() { + sliderValue = value; + }); + await player.seek(duration * value.clamp(0.0, 1.0)); + }, + onChangeEnd: (value) async { + setState(() { + sliderDraging = false; + }); + if (!player.state.playing) { + await player.play(); + } + }, + ), + ), + onPointerMove: (e) { + setState(() { + sliderDraging = true; + }); + }, + ), + ), + ), + Positioned( + bottom: 100.0, + left: 10.0, + right: 10.0, + child: Visibility( + visible: sliderDraging, + child: DefaultTextStyle( + style: TextStyle(color: Colors.white54, fontSize: 18.0, fontFamily: 'Arial'), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8.0, + children: [ + Text(position.label(reference: duration), style: TextStyle(color: Colors.white)), + Text('/', style: TextStyle(fontSize: 14.0)), + Text(duration.label(reference: duration)), + ], + ), + )), + ), + ], + ); + }, + ), + ], + ), ), - ), ], ), ); diff --git a/lib/pages/video/module/recommend.dart b/lib/pages/video/module/recommend.dart index d6d55a6..273116f 100644 --- a/lib/pages/video/module/recommend.dart +++ b/lib/pages/video/module/recommend.dart @@ -9,6 +9,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_core.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_service.dart' hide logger; @@ -16,12 +17,12 @@ import 'package:loopin/api/video_api.dart'; import 'package:loopin/components/my_toast.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/models/conversation_type.dart'; +import 'package:loopin/models/share_type.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/utils/download_video.dart'; import 'package:loopin/utils/permissions.dart'; import 'package:loopin/utils/wxsdk.dart'; -import 'package:loopin/models/share_type.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart'; @@ -980,15 +981,20 @@ class _RecommendModuleState extends State { void handlCoverClick(V2TimConversation conv) async { // 发送VideoMsg,获取当前视频信息 final userId = conv.userID; - final String url = videoList[videoModuleController.videoPlayIndex.value]['url']; - final img = videoList[videoModuleController.videoPlayIndex.value]['firstFrameImg']; - final width = videoList[videoModuleController.videoPlayIndex.value]['width']; - final height = videoList[videoModuleController.videoPlayIndex.value]['height']; + final currentVideo = videoList[videoModuleController.videoPlayIndex.value]; + logger.w(currentVideo); + final img = (currentVideo['cover'] != null && currentVideo['cover'].toString().isNotEmpty) ? currentVideo['cover'] : currentVideo['firstFrameImg']; + final url = currentVideo['url']; + final width = currentVideo['width']; + final height = currentVideo['height']; + final videoId = currentVideo['id']; final makeJson = jsonEncode({ "width": width, "height": height, "imgUrl": img, "videoUrl": url, + "videoId": videoId, + "userID": Get.find().userID.value, }); final res = await IMMessage().createCustomMessage( data: makeJson, diff --git a/lib/router/index.dart b/lib/router/index.dart index 5d4a735..920cfb2 100644 --- a/lib/router/index.dart +++ b/lib/router/index.dart @@ -10,6 +10,7 @@ import 'package:loopin/pages/chat/chat_no_friend.dart'; import 'package:loopin/pages/chat/notify/interaction.dart'; import 'package:loopin/pages/chat/notify/noFriend.dart'; import 'package:loopin/pages/chat/notify/system.dart'; +import 'package:loopin/pages/groupChat/groupList.dart'; import 'package:loopin/pages/groupChat/index.dart'; import 'package:loopin/pages/my/des.dart'; import 'package:loopin/pages/my/fans.dart'; @@ -67,8 +68,8 @@ final Map routes = { '/flow': const Flowing(), '/eachFlow': const MutualFollowers(), //群 - '/group': const StartGroupChatPage(), - //关系链 + '/group': const StartGroupChatPage(), // 创建群聊 + '/groupList': const Grouplist(), // 已入群列表 }; final List routeList = routes.entries diff --git a/lib/utils/parse_message_summary.dart b/lib/utils/parse_message_summary.dart index a0c6adb..582466a 100644 --- a/lib/utils/parse_message_summary.dart +++ b/lib/utils/parse_message_summary.dart @@ -51,10 +51,24 @@ String parseMessageSummary(V2TimMessage msg) { // groupNotifyLeaveUp, // 群通知->群升级为达人群通知 String _parseCustomMessage(V2TimMessage? msg) { if (msg == null) return '[null]'; - final sum = msg.cloudCustomData; + String sum; + // 客户端本地用的是字符串,服务端返回的是JSON(服务端改cloudCustomData的类型为String,或者客户端代码修改发送消息时携带的参数由string变为json) + final raw = msg.cloudCustomData; + if (raw == null) { + sum = ''; + } else { + try { + final decoded = jsonDecode(raw); + sum = decoded is String ? decoded : raw; + } catch (_) { + sum = raw; + } + } + // final sum = jsonDecode(msg.cloudCustomData!); final elment = msg.customElem; // 所有服务端发送的通知消息都走【自定义消息类型】 - logger.w('解析自定义消息:$sum,自定义属性:${msg.cloudCustomData}'); - // logger.w('解析element:${elment?.desc ?? 'summary_error'}'); + // logger.w('解析自定义消息:$sum,自定义属性:${msg.cloudCustomData}'); + logger.w(sum); + logger.w('解析element:${elment?.desc ?? 'summary_error'}'); try { switch (sum) { case SummaryType.hongbao: diff --git a/pubspec.lock b/pubspec.lock index 1278df3..a59ec96 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -249,6 +249,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" + dynamic_tabbar: + dependency: "direct main" + description: + name: dynamic_tabbar + sha256: "017be3705f70e353e579b7a7d56bb19b5c112072b4532bc8bd68767c4a6fc3fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.9" easy_refresh: dependency: "direct main" description: @@ -598,69 +606,69 @@ packages: source: hosted version: "4.5.4" image_picker: - dependency: transitive + dependency: "direct main" description: name: image_picker - sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.2" + version: "1.2.0" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d" + sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.12+24" + version: "0.8.13+1" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.6" + version: "3.1.0" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.12+2" + version: "0.8.13" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.1+2" + version: "0.2.2" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.1+2" + version: "0.2.2" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" url: "https://pub.flutter-io.cn" source: hosted - version: "2.10.1" + version: "2.11.0" image_picker_windows: dependency: transitive description: name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.1+1" + version: "0.2.2" install_plugin: dependency: "direct main" description: @@ -846,7 +854,7 @@ packages: source: hosted version: "1.16.0" mime: - dependency: transitive + dependency: "direct main" description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" @@ -1419,7 +1427,7 @@ packages: source: hosted version: "2.1.4" video_player: - dependency: transitive + dependency: "direct main" description: name: video_player sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" diff --git a/pubspec.yaml b/pubspec.yaml index 2f6e8db..1fc8843 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: photo_view: ^0.15.0 shirne_dialog: ^4.8.3 package_info_plus: ^8.3.0 + dynamic_tabbar: ^1.0.9 #tab flutter_upgrader: ^1.1.20 #更新 path_provider: ^2.1.2 @@ -88,6 +89,9 @@ dependencies: audioplayers: ^6.5.0 #音频播放 flutter_html: ^3.0.0 timer_count_down: ^2.2.2 #倒计时 + image_picker: ^1.2.0 #相机 + video_player: ^2.10.0 #视频处理 + mime: ^2.0.0 #文件类型推断 dev_dependencies: flutter_launcher_icons: ^0.13.1 # 使用最新版本