/// 精选推荐模块 library; import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; 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/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: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'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import '../../../behavior/custom_scroll_behavior.dart'; import '../../../controller/video_module_controller.dart'; import '../../../router/fade_route.dart'; import '../components/popup_reply.dart'; class RecommendModule extends StatefulWidget { const RecommendModule({super.key}); static Player? _player; static void setPlayer(Player player) { _player = player; } static void pauseVideo() { _player?.pause(); } static void playVideo() { _player?.play(); } @override State createState() => _RecommendModuleState(); } class CommentBottomSheet extends StatefulWidget { final String videoId; final Function(int) onCommentCountChanged; // 新增回调函数 const CommentBottomSheet({ super.key, required this.videoId, required this.onCommentCountChanged, // 接收回调 }); @override _CommentBottomSheetState createState() => _CommentBottomSheetState(); } class _CommentBottomSheetState extends State { // 评论相关状态(移动到内部) int commentPage = 1; final int commentPageSize = 10; bool isLoadingMoreComments = false; bool hasMoreComments = true; List> commentList = []; String replyingCommentId = ''; String replyingCommentUser = ''; // 新增:子评论相关状态 Map expandedReplies = {}; // 存储已展开的回复 {commentId: true/false} Map>> replyData = {}; // 存储子评论数据 {commentId: [replies]} Map replyPage = {}; // 子评论分页 Map isLoadingReplies = {}; // 子评论加载状态 @override void initState() { super.initState(); // 初始化时加载评论 fetchComments(false); } Future fetchComments(bool loadMore) async { if (isLoadingMoreComments && !loadMore) return; setState(() { isLoadingMoreComments = true; }); if (!loadMore) { commentPage = 1; hasMoreComments = true; commentList.clear(); } logger.d('入参vlogId-------------------->: ${widget.videoId}'); try { final res = await Http.post(VideoApi.videoCommentList, data: { 'vlogId': widget.videoId, 'current': commentPage, 'size': commentPageSize, }); logger.d('评论接口列表返回: ${json.encode(res)}'); if (res['code'] == 200 && res['data'] != null) { final data = res['data']; final List> newComments = List>.from(data['records'] ?? []); final int total = data['total'] ?? 0; setState(() { if (loadMore) { commentList.addAll(newComments); } else { commentList = newComments; } hasMoreComments = commentList.length < total; commentPage++; }); widget.onCommentCountChanged(total); logger.d('成功加载 ${newComments.length} 条评论,总共 $total 条'); } } catch (e) { logger.e('获取评论异常: $e'); } finally { setState(() { isLoadingMoreComments = false; }); } } Future postComment(String content, {String parentCommentId = ''}) async { try { final res = await Http.post(VideoApi.doVideoComment, data: { 'vlogId': widget.videoId, 'content': content, 'fatherCommentId': parentCommentId.isNotEmpty ? parentCommentId : null, }); if (res['code'] == 200) { // 如果是回复,刷新对应的子评论 if (parentCommentId.isNotEmpty) { fetchReplies(parentCommentId, false); // 更新主评论的子评论数量 setState(() { final comment = commentList.firstWhere((c) => c['id'] == parentCommentId, orElse: () => {}); if (comment.isNotEmpty) { comment['childCount'] = (comment['childCount'] ?? 0) + 1; } }); } else { // 如果是新评论,刷新整个列表 fetchComments(false); } widget.onCommentCountChanged(commentList.length + 1); MyToast().tip( title: '评论成功', position: 'center', type: 'success', ); } } catch (e) { logger.i('发布评论失败: $e'); MyToast().tip( title: '评论失败', position: 'center', type: 'error', ); } } // 获取二级评论,复用一级评论的接口,修改传参 Future fetchReplies(String commentId, bool loadMore) async { if (isLoadingReplies[commentId] == true && !loadMore) return; setState(() { isLoadingReplies[commentId] = true; }); if (!loadMore) { replyPage[commentId] = 1; replyData[commentId] = []; } try { final res = await Http.post(VideoApi.videoCommentList, data: { 'fatherCommentId': commentId, 'current': replyPage[commentId], 'size': commentPageSize, }); if (res['code'] == 200 && res['data'] != null) { final data = res['data']; final List> newReplies = List>.from(data['records'] ?? []); setState(() { if (loadMore) { replyData[commentId]!.addAll(newReplies); } else { replyData[commentId] = newReplies; } replyPage[commentId] = replyPage[commentId]! + 1; }); } } catch (e) { logger.e('获取子评论异常: $e'); } finally { setState(() { isLoadingReplies[commentId] = false; }); } } @override Widget build(BuildContext context) { return Material( color: Colors.white, child: Column( children: [ Container( padding: EdgeInsets.fromLTRB(15.0, 10.0, 10.0, 5.0), decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFFAFAFA)))), child: Column( spacing: 10.0, children: [ Row( children: [ /* Expanded( child: Text.rich(TextSpan(children: [ TextSpan( text: '大家都在搜: ', style: TextStyle(color: Colors.grey), ), TextSpan( text: '黑神话-悟空', style: TextStyle(color: const Color(0xFF496D80)), ), ]))),*/ GestureDetector( child: Container( height: 22.0, width: 22.0, decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(100.0), ), child: UnconstrainedBox(child: Icon(Icons.close, color: Colors.black45, size: 14.0))), onTap: () { Navigator.pop(context); }, ), ], ), Text( '${commentList.length}条评论', style: TextStyle(fontSize: 12.0, fontWeight: FontWeight.w600), ), ], ), ), Expanded( child: NotificationListener( onNotification: (ScrollNotification scrollInfo) { if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !isLoadingMoreComments && hasMoreComments) { fetchComments(true); } return false; }, child: commentList.isEmpty && !isLoadingMoreComments ? Center( child: Text( '暂无评论', style: TextStyle(color: Colors.grey), ), ) : ListView.builder( physics: BouncingScrollPhysics(), itemCount: commentList.length + (hasMoreComments ? 1 : 0), itemBuilder: (context, index) { if (index == commentList.length) { return Center( child: Padding( padding: EdgeInsets.all(16.0), child: isLoadingMoreComments ? CircularProgressIndicator() : Text('没有更多评论了'), ), ); } final comment = commentList[index]; final hasReplies = comment['childCount'] > 0; final isExpanded = expandedReplies[comment['id']] == true; final replies = replyData[comment['id']] ?? []; return Column( children: [ // 主评论 ListTile( isThreeLine: true, leading: ClipRRect( borderRadius: BorderRadius.circular(50.0), child: NetworkOrAssetImage( imageUrl: comment['commentUserFace'] ?? 'assets/images/avatar/default.png', width: 30.0, height: 30.0, fit: BoxFit.cover, ), ), title: Row( children: [ Expanded( child: Text( comment['commentUserNickname'] ?? '未知用户', style: TextStyle( color: Colors.grey, fontSize: 12.0, ), ), ), SizedBox(width: 20.0), GestureDetector( onTap: () { setState(() { replyingCommentId = comment['id']; replyingCommentUser = comment['commentUserNickname'] ?? '未知用户'; }); }, child: Icon( Icons.reply, color: Colors.black54, size: 16.0, ), ), ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: EdgeInsets.symmetric(vertical: 5.0), child: Text( comment['content'] ?? '', style: TextStyle( fontSize: 14.0, ), ), ), if (hasReplies) GestureDetector( onTap: () { setState(() { expandedReplies[comment['id']] = !isExpanded; if (expandedReplies[comment['id']] == true && (replyData[comment['id']] == null || replyData[comment['id']]!.isEmpty)) { fetchReplies(comment['id'], false); } }); }, child: Container( margin: EdgeInsets.only(right: 15.0), padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 3.0), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(20.0), ), child: Row(children: [ Text( '${comment['childCount']}回复', style: TextStyle(fontSize: 12.0), ), Icon( isExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 16.0, ) ]), ), ), Text( comment['createTime']?.toString().substring(0, 10) ?? '', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), ), // 子评论列表 if (isExpanded && replies.isNotEmpty) Padding( padding: EdgeInsets.only(left: 40.0), child: Column( children: [ ...replies.map((reply) => ListTile( leading: ClipRRect( borderRadius: BorderRadius.circular(25.0), child: NetworkOrAssetImage( imageUrl: reply['commentUserFace'] ?? 'assets/images/avatar/default.png', width: 25.0, height: 25.0, fit: BoxFit.cover, ), ), title: Row( children: [ Expanded( child: Text( reply['commentUserNickname'] ?? '未知用户', style: TextStyle( color: Colors.grey, fontSize: 11.0, ), ), ), GestureDetector( onTap: () { setState(() { replyingCommentId = comment['id']; replyingCommentUser = reply['commentUserNickname'] ?? '未知用户'; }); }, child: Icon( Icons.reply, color: Colors.black54, size: 14.0, ), ), ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: EdgeInsets.symmetric(vertical: 3.0), child: Text( reply['content'] ?? '', style: TextStyle(fontSize: 13.0), ), ), Text( reply['createTime']?.toString().substring(0, 10) ?? '', style: TextStyle(color: Colors.grey, fontSize: 11.0), ), ], ), )), // 加载更多子评论按钮 if (replies.length < comment['childCount']) Center( child: TextButton( onPressed: () => fetchReplies(comment['id'], true), child: isLoadingReplies[comment['id']] == true ? CircularProgressIndicator() : Text('加载更多回复'), ), ), ], ), ), Divider(height: 1, color: Colors.grey[200]), ], ); }, ), ), ), // 在回复输入区域显示更详细的信息 if (replyingCommentId.isNotEmpty) Container( padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), color: Colors.grey[100], child: Row( children: [ Expanded( child: Text( '回复 @$replyingCommentUser', style: TextStyle(fontSize: 12.0, color: Colors.blue), overflow: TextOverflow.ellipsis, ), ), GestureDetector( onTap: () { setState(() { replyingCommentId = ''; replyingCommentUser = ''; }); }, child: Icon(Icons.close, size: 16.0), ), ], ), ), GestureDetector( child: Container( margin: EdgeInsets.all(10.0), height: 40.0, decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(30.0), ), child: Row( children: [ SizedBox(width: 15.0), Icon( Icons.edit_note, color: Colors.black54, size: 16.0, ), SizedBox(width: 5.0), Text( replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...', style: TextStyle(color: Colors.black54, fontSize: 14.0), ), ], ), ), onTap: () { Navigator.push( context, FadeRoute( child: PopupReply( hintText: replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...', onChanged: (value) { debugPrint('评论内容: $value'); }, onSubmitted: (value) { if (value.isNotEmpty) { postComment(value, parentCommentId: replyingCommentId); setState(() { replyingCommentId = ''; replyingCommentUser = ''; }); Navigator.pop(context); } }, ))); }, ), ], ), ); } } class _RecommendModuleState extends State { VideoModuleController videoModuleController = Get.put(VideoModuleController()); final ChatController chatController = Get.find(); final RxMap lastReturnData = RxMap({}); // 分页内容 int page = 1; final int pageSize = 10; bool isLoadingMore = false; // 页面controller late PageController pageController = PageController( initialPage: videoModuleController.videoPlayIndex.value, viewportFraction: 1.0, ); // 播放器controller late Player player = Player(); late VideoController videoController = VideoController(player); final List subscriptions = []; // 进度条slider当前阈值 double sliderValue = 0.0; bool sliderDraging = false; late Duration position = Duration.zero; // 当前时长 late Duration duration = Duration.zero; // 总时长 // 视频数据 List videoList = []; // 评论相关状态 int commentPage = 1; final int commentPageSize = 10; bool isLoadingMoreComments = false; bool hasMoreComments = true; List> commentList = []; int currentVideoId = 0; String replyingCommentId = ''; // 正在回复的评论ID String replyingCommentUser = ''; // 正在回复的用户名 // 分享列表 List shareList = [ {'icon': 'assets/images/share-wx.png', 'label': '好友'}, {'icon': 'assets/images/share-pyq.png', 'label': '朋友圈'}, {'icon': 'assets/images/share-link.png', 'label': '复制链接'}, {'icon': 'assets/images/share-download.png', 'label': '下载'}, ]; @override void initState() { super.initState(); videoModuleController.needRefresh.listen((need) { if (need) { reInit(); videoModuleController.clearNeedRefresh(); } }); RecommendModule.setPlayer(player); // 获取视频数据 fetchVideoList(); } @override void setState(VoidCallback fn) { if (mounted) { super.setState(fn); } } @override void didChangeDependencies() { super.didChangeDependencies(); if (subscriptions.isEmpty) { subscriptions.addAll( [ // 监听视频时长 player.stream.duration.listen((event) { setState(() { duration = event; }); }), // 监听视频播放进度 player.stream.position.listen((event) { setState(() { position = event; if (position > Duration.zero && !sliderDraging) { // 设置视频播放位置 sliderValue = (position.inMilliseconds / duration.inMilliseconds).clamp(0.0, 1.0); } }); }), ], ); } } @override void dispose() { player.dispose(); pageController.dispose(); for (final subscription in subscriptions) { subscription.cancel(); } super.dispose(); } void reInit() async { await player.stop(); // 重置状态 page = 1; isLoadingMore = false; videoList.clear(); videoModuleController.updateVideoPlayIndex(0); sliderValue = 0.0; sliderDraging = false; position = Duration.zero; duration = Duration.zero; pageController.jumpToPage(0); // 拉新数据 fetchVideoList(); } Future fetchVideoList() async { if (isLoadingMore) return; isLoadingMore = true; try { final res = await Http.post(VideoApi.vlogList, data: { 'current': page, 'size': pageSize, }); final data = res['data']; // logger.d('关注用户的视频列表:$data'); if (data == null || (data is List && data.isEmpty)) { return; } print('推荐视频列表------------------>${data['records']}'); if (data['records'] is List) { List videos = data['records']; for (var item in videos) { item['expanded'] = false; } setState(() { if (page == 1) { // 初始化 videoList = videos; } else { videoList.addAll(videos); } }); // 处理完成后 if (videos.isNotEmpty) { page++; } // logger.i('视频数据列表------------------->$videoList'); // 初始化播放器 player.open( Media( videoList[videoModuleController.videoPlayIndex.value]['url'], ), play: false); player.setPlaylistMode(PlaylistMode.loop); // 循环播放; // 第一次加载后播放第一个视频 if (page == 2 && videoModuleController.videoTabIndex.value == 2 && Get.currentRoute == '/' && videoModuleController.layoutPageCurrent.value == 0) { player.play(); // 播放第一个 } else { logger.i('没播放视频'); } } } catch (e) { logger.i('获取视频失败: $e'); } finally { isLoadingMore = false; // 加载完成,标记为 false } } // 取消喜欢视频 Future doUnLikeVideo(item) async { logger.d('点击了点赞按钮$item'); try { final res = await Http.post(VideoApi.unlike, data: {'id': item['id']}); final resCode = res['code']; if (resCode == 200) { item['doILikeThisVlog'] = !item['doILikeThisVlog']; } } catch (e) { logger.i('点击取消喜欢按钮报错: $e'); } } // 点击喜欢视频 Future doLikeVideo(item) async { try { final res = await Http.post(VideoApi.like, data: {'id': item['id']}); final resCode = res['code']; if (resCode == 200) { item['doILikeThisVlog'] = !item['doILikeThisVlog']; } logger.i('点赞返回信息----------->: $res'); } catch (e) { logger.i('点击喜欢按钮报错: $e'); } } // 评论弹框 void handleComment(index) { final videoId = videoList[index]['id']; logger.i('点击了评论按钮$videoId'); showModalBottomSheet( backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))), showDragHandle: false, clipBehavior: Clip.antiAlias, isScrollControlled: true, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 3 / 4, ), context: context, builder: (context) { return CommentBottomSheet( videoId: videoId, onCommentCountChanged: (newCount) { // 更新对应视频的评论数量 setState(() { if (index < videoList.length) { videoList[index]['commentsCounts'] = newCount; } }); }); }, ); } // 分享弹框 void handleShare(index) { if (chatController.chatList.isNotEmpty) { chatController.getConversationList(); } showModalBottomSheet( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)), ), clipBehavior: Clip.antiAlias, context: context, isScrollControlled: true, builder: (context) { final filteredList = chatController.chatList.where((item) => conversationTypeFromString(item.isCustomAdmin) == null).toList(); return Material( color: Colors.white, child: Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 分享列表 SizedBox( height: 110, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: shareList.length, padding: EdgeInsets.symmetric(horizontal: 0, vertical: 20.0), itemBuilder: (context, index) { return GestureDetector( onTap: () => handleShareClick(index), child: Container( width: 64, margin: EdgeInsets.symmetric(horizontal: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset('${shareList[index]['icon']}', width: 48.0), SizedBox(height: 5), Text( '${shareList[index]['label']}', style: TextStyle(fontSize: 12.0), overflow: TextOverflow.ellipsis, ), ], ), ), ); }, ), ), // 会话列表 if (filteredList.isNotEmpty) SizedBox( height: 110, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: filteredList.length, padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20.0), itemBuilder: (context, index) { return GestureDetector( onTap: () => handlCoverClick(filteredList[index].conversation), child: Container( width: 64, margin: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ ClipOval( child: NetworkOrAssetImage( imageUrl: filteredList[index].faceUrl, width: 48.0, height: 48.0, ), ), const SizedBox(height: 5), Text( filteredList[index].conversation.showName ?? '', style: const TextStyle(fontSize: 12.0), overflow: TextOverflow.ellipsis, ), ], ), ), ); }, ), ), // 取消按钮 SafeArea( top: false, child: InkWell( onTap: () => Get.back(), child: Container( alignment: Alignment.center, width: double.infinity, height: 50.0, color: Colors.grey[50], child: Text( '取消', style: TextStyle(color: Colors.black87), ), ), ), ), ], ), ), ); }, ); } void handleShareClick(int index) { print("分享项 $index 被点击"); final videoId = videoList[videoModuleController.videoPlayIndex.value]['id']; final videoUrl = videoList[videoModuleController.videoPlayIndex.value]['url']; final description = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '快来看看这个视频'; logger.i('分享链接地址----------------: ${ShareType.video.name}?id=$videoId'); if (index == 0) { // 分享好友 Wxsdk.shareToFriend(title: '快来看看这个视频', description: description, webpageUrl: '${ShareType.video.name}?id=$videoId'); } else if (index == 1) { // 分享到朋友圈 Wxsdk.shareToTimeline(title: '快来看看这个视频', webpageUrl: '${ShareType.video.name}?id=$videoId'); } else if (index == 2) { // 复制链接到剪切板 copyToClipboard(videoUrl); } else if (index == 3) { // 下载视频到本地 _downloadVideoWithDio(videoUrl, description); } } // 复制链接到剪贴板 void copyToClipboard(String text) async { try { await Clipboard.setData(ClipboardData(text: text)); MyToast().tip( title: '链接已复制到剪贴板', position: 'center', type: 'success', ); } catch (e) { MyToast().tip( title: '复制失败', position: 'center', type: 'success', ); } } // 下载视频 Future _downloadVideoWithDio(String videoUrl, String fileName) async { try { // 请求存储权限 String? toastId; // 用于存储toast的ID,以便后续关闭 var status = await Permissions.requestStoragePermission(); if (!status) { MyToast().tip( title: '需要存储权限才能下载视频', position: 'center', type: 'success', ); return; } await DownloadManager.downloadFile( url: videoUrl, fileName: '$fileName.mp4', onProgress: (progress) { print("下载进度: $progress%"); // 显示进度组件 }, onComplete: (filePath) { MyToast().tip( title: '下载完成', position: 'center', type: 'success', ); }, onError: (error) { MyToast().tip( title: '下载失败: $error', position: 'center', type: 'error', ); }, ); } catch (e) { print("下载视频失败: $e"); } } void handlCoverClick(V2TimConversation conv) async { // 发送VideoMsg,获取当前视频信息 final userId = conv.userID; 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, ); if (res.success) { final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareVideo); if (sendRes.success) { MyToast().tip( title: '分享成功', position: 'center', type: 'success', ); Get.back(); } else { logger.e(res.desc); } } else { logger.e(res.desc); } } @override Widget build(BuildContext context) { return Container( color: Colors.black, child: Column( children: [ 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( 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'], ), ), ), ), ), ), 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)), ], ), )), ), ], ); }, ), ], ), ), ], ), ); } }