flutter/lib/pages/video/module/friend.dart
cuiyouliang be7a137b07 1、订单修改
2、增加好友视频
2025-09-05 10:55:38 +08:00

1420 lines
60 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// 朋友视频列表
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/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/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 FriendModule extends StatefulWidget {
const FriendModule({super.key});
static Player? _player;
static void setPlayer(Player player) {
_player = player;
}
static void pauseVideo() {
_player?.pause();
}
static void playVideo() {
_player?.play();
}
@override
State<FriendModule> createState() => _FriendModuleState();
}
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<CommentBottomSheet> {
// 评论相关状态(移动到内部)
int commentPage = 1;
final int commentPageSize = 10;
bool isLoadingMoreComments = false;
bool hasMoreComments = true;
List<Map<String, dynamic>> commentList = [];
String replyingCommentId = '';
String replyingCommentUser = '';
// 新增:子评论相关状态
Map<String, dynamic> expandedReplies = {}; // 存储已展开的回复 {commentId: true/false}
Map<String, List<Map<String, dynamic>>> replyData = {}; // 存储子评论数据 {commentId: [replies]}
Map<String, int> replyPage = {}; // 子评论分页
Map<String, bool> isLoadingReplies = {}; // 子评论加载状态
@override
void initState() {
super.initState();
// 初始化时加载评论
fetchComments(false);
}
Future<void> 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<Map<String, dynamic>> newComments = List<Map<String, dynamic>>.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<void> 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: () => <String, dynamic>{});
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<void> 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<Map<String, dynamic>> newReplies = List<Map<String, dynamic>>.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<ScrollNotification>(
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 _FriendModuleState extends State<FriendModule> {
VideoModuleController videoModuleController = Get.put(VideoModuleController());
final ChatController chatController = Get.find<ChatController>();
final RxMap<String, dynamic> 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<StreamSubscription> 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<Map<String, dynamic>> 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();
}
});
FriendModule.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<void> fetchVideoList() async {
if (isLoadingMore) return;
isLoadingMore = true;
try {
final res = await Http.post(VideoApi.friendList, data: {
'current': page,
'size': pageSize,
});
final data = res['data'];
logger.d('好友的视频列表:$data');
// 处理空数据情况
if (data == null || data['records'] == null || (data['records'] is List && data['records'].isEmpty)) {
setState(() {
videoList = []; // 清空视频列表
});
return;
}
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<void> 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<void> 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) {
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 (chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).isNotEmpty)
SizedBox(
height: 110,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).length,
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20.0),
itemBuilder: (context, index) {
final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList();
return GestureDetector(
onTap: () => handlCoverClick(filteredList[index].conversation),
child: Container(
width: 64,
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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'] ?? '快来看看这个视频';
var httpPrefix = 'http://43.143.227.203/adv';
logger.i('分享链接地址----------------: $httpPrefix/goods-detail?id=$videoId');
if (index == 0) {
// 分享好友
Wxsdk.shareToFriend(title: '快来看看这个视频', description: description, webpageUrl: '$httpPrefix/video-detail?id=$videoId');
} else if (index == 1) {
// 分享到朋友圈
Wxsdk.shareToTimeline(title: '快来看看这个视频', webpageUrl: '$httpPrefix/goods-detail?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<void> _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 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 makeJson = jsonEncode({
"width": width,
"height": height,
"imgUrl": img,
"videoUrl": url,
});
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: [
// 添加暂无数据提示
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;
});
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)),
],
),
)),
),
],
);
},
),
],
),
),
],
),
);
}
}