flutter/lib/pages/video/module/recommend.dart

1406 lines
59 KiB
Dart
Raw Normal View History

2025-07-21 15:46:30 +08:00
/// 精选推荐模块
library;
import 'dart:async';
2025-08-21 10:50:38 +08:00
import 'dart:convert';
2025-07-21 15:46:30 +08:00
2025-08-26 15:22:16 +08:00
import 'package:flutter/services.dart';
2025-07-21 15:46:30 +08:00
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
2025-08-21 10:50:38 +08:00
import 'package:loopin/IM/controller/chat_controller.dart';
2025-07-21 15:46:30 +08:00
import 'package:loopin/IM/im_core.dart';
2025-08-21 10:50:38 +08:00
import 'package:loopin/IM/im_message.dart';
2025-08-25 18:07:40 +08:00
import 'package:loopin/IM/im_service.dart' hide logger;
2025-07-21 15:46:30 +08:00
import 'package:loopin/api/video_api.dart';
2025-08-21 10:50:38 +08:00
import 'package:loopin/components/my_toast.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/models/summary_type.dart';
2025-07-21 15:46:30 +08:00
import 'package:loopin/service/http.dart';
2025-08-21 10:50:38 +08:00
import 'package:loopin/utils/wxsdk.dart';
2025-08-26 15:22:16 +08:00
import 'package:loopin/utils/permissions.dart';
import 'package:loopin/utils/download_video.dart';
2025-07-21 15:46:30 +08:00
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';
2025-08-21 10:50:38 +08:00
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
2025-07-21 15:46:30 +08:00
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<RecommendModule> createState() => _RecommendModuleState();
}
2025-08-22 18:24:52 +08:00
class CommentBottomSheet extends StatefulWidget {
2025-08-25 18:07:40 +08:00
final String videoId;
final Function(int) onCommentCountChanged; // 新增回调函数
2025-07-21 15:46:30 +08:00
2025-08-25 18:07:40 +08:00
const CommentBottomSheet({
super.key,
required this.videoId,
required this.onCommentCountChanged, // 接收回调
});
2025-08-22 18:24:52 +08:00
@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 = [];
2025-08-25 18:07:40 +08:00
String replyingCommentId = '';
2025-08-22 18:24:52 +08:00
String replyingCommentUser = '';
2025-08-25 18:07:40 +08:00
// 新增:子评论相关状态
Map<String, dynamic> expandedReplies = {}; // 存储已展开的回复 {commentId: true/false}
Map<String, List<Map<String, dynamic>>> replyData = {}; // 存储子评论数据 {commentId: [replies]}
Map<String, int> replyPage = {}; // 子评论分页
Map<String, bool> isLoadingReplies = {}; // 子评论加载状态
2025-08-22 18:24:52 +08:00
@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();
}
2025-08-25 18:07:40 +08:00
logger.d('入参vlogId-------------------->: ${widget.videoId}');
2025-08-22 18:24:52 +08:00
try {
final res = await Http.post(VideoApi.videoCommentList, data: {
'vlogId': widget.videoId,
'current': commentPage,
'size': commentPageSize,
});
2025-08-25 18:07:40 +08:00
logger.d('评论接口列表返回: ${json.encode(res)}');
2025-08-22 18:24:52 +08:00
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++;
});
2025-08-25 18:07:40 +08:00
widget.onCommentCountChanged(total);
2025-08-22 18:24:52 +08:00
logger.d('成功加载 ${newComments.length} 条评论,总共 $total');
}
} catch (e) {
logger.e('获取评论异常: $e');
} finally {
setState(() {
isLoadingMoreComments = false;
});
}
}
2025-08-25 18:07:40 +08:00
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,
});
2025-08-22 18:24:52 +08:00
2025-08-25 18:07:40 +08:00
if (res['code'] == 200) {
// 如果是回复,刷新对应的子评论
if (parentCommentId.isNotEmpty) {
fetchReplies(parentCommentId, false);
// 更新主评论的子评论数量
setState(() {
final comment = commentList.firstWhere(
(c) => c['id'] == parentCommentId,
orElse: () => <String, dynamic>{}
);
if (comment != null && comment.isNotEmpty) {
comment['childCount'] = (comment['childCount'] ?? 0) + 1;
}
});
} else {
// 如果是新评论,刷新整个列表
2025-08-22 18:24:52 +08:00
fetchComments(false);
}
2025-08-25 18:07:40 +08:00
widget.onCommentCountChanged(commentList.length + 1);
2025-08-22 18:24:52 +08:00
MyToast().tip(
2025-08-25 18:07:40 +08:00
title: '评论成功',
2025-08-22 18:24:52 +08:00
position: 'center',
2025-08-25 18:07:40 +08:00
type: 'success',
2025-08-22 18:24:52 +08:00
);
}
2025-08-25 18:07:40 +08:00
} 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;
});
2025-08-22 18:24:52 +08:00
}
2025-08-25 18:07:40 +08:00
}
2025-08-22 18:24:52 +08:00
@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];
2025-08-25 18:07:40 +08:00
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,
2025-08-22 18:24:52 +08:00
),
),
2025-08-25 18:07:40 +08:00
title: Row(
children: [
Expanded(
child: Text(
comment['commentUserNickname'] ?? '未知用户',
style: TextStyle(
color: Colors.grey,
fontSize: 12.0,
),
),
2025-08-22 18:24:52 +08:00
),
2025-08-25 18:07:40 +08:00
SizedBox(width: 20.0),
GestureDetector(
onTap: () {
setState(() {
replyingCommentId = comment['id'];
replyingCommentUser = comment['commentUserNickname'] ?? '未知用户';
});
},
child: Icon(
Icons.reply,
color: Colors.black54,
size: 16.0,
),
),
],
2025-08-22 18:24:52 +08:00
),
2025-08-25 18:07:40 +08:00
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.symmetric(vertical: 5.0),
child: Text(
comment['content'] ?? '',
style: TextStyle(
fontSize: 14.0,
),
2025-08-22 18:24:52 +08:00
),
2025-08-25 18:07:40 +08:00
),
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,
)
]),
2025-08-22 18:24:52 +08:00
),
2025-08-25 18:07:40 +08:00
),
Text(
'${comment['createTime']?.toString().substring(0, 10) ?? ''}',
style: TextStyle(color: Colors.grey, fontSize: 12.0),
2025-08-22 18:24:52 +08:00
),
2025-08-25 18:07:40 +08:00
],
),
),
// 子评论列表
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),
),
],
),
)).toList(),
// 加载更多子评论按钮
if (replies.length < comment['childCount'])
Center(
child: TextButton(
onPressed: () => fetchReplies(comment['id'], true),
child: isLoadingReplies[comment['id']] == true
? CircularProgressIndicator()
: Text('加载更多回复'),
),
),
],
2025-08-22 18:24:52 +08:00
),
),
2025-08-25 18:07:40 +08:00
Divider(height: 1, color: Colors.grey[200]),
],
2025-08-22 18:24:52 +08:00
);
},
),
2025-08-25 18:07:40 +08:00
),
2025-08-22 18:24:52 +08:00
),
2025-08-25 18:07:40 +08:00
// 在回复输入区域显示更详细的信息
if (replyingCommentId.isNotEmpty)
2025-08-22 18:24:52 +08:00
Container(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
color: Colors.grey[100],
child: Row(
children: [
2025-08-25 18:07:40 +08:00
Expanded(
child: Text(
'回复 @$replyingCommentUser',
style: TextStyle(fontSize: 12.0, color: Colors.blue),
overflow: TextOverflow.ellipsis,
),
2025-08-22 18:24:52 +08:00
),
GestureDetector(
onTap: () {
setState(() {
2025-08-25 18:07:40 +08:00
replyingCommentId = '';
2025-08-22 18:24:52 +08:00
replyingCommentUser = '';
});
},
child: Icon(Icons.close, size: 16.0),
),
],
),
),
2025-08-25 18:07:40 +08:00
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),
),
],
),
2025-08-22 18:24:52 +08:00
),
2025-08-25 18:07:40 +08:00
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);
}
},
)));
},
2025-08-22 18:24:52 +08:00
),
2025-08-25 18:07:40 +08:00
],
),
);
}
2025-08-22 18:24:52 +08:00
}
2025-07-21 15:46:30 +08:00
class _RecommendModuleState extends State<RecommendModule> {
VideoModuleController videoModuleController = Get.put(VideoModuleController());
2025-08-21 10:50:38 +08:00
final ChatController chatController = Get.find<ChatController>();
2025-08-25 18:07:40 +08:00
final RxMap<String, dynamic> lastReturnData = RxMap({});
2025-07-21 15:46:30 +08:00
// 分页内容
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 = [];
2025-08-22 18:24:52 +08:00
// 评论相关状态
int commentPage = 1;
final int commentPageSize = 10;
bool isLoadingMoreComments = false;
bool hasMoreComments = true;
List<Map<String, dynamic>> commentList = [];
int currentVideoId = 0;
2025-08-25 18:07:40 +08:00
String replyingCommentId = ''; // 正在回复的评论ID
2025-08-22 18:24:52 +08:00
String replyingCommentUser = ''; // 正在回复的用户名
2025-07-21 15:46:30 +08:00
// 分享列表
List shareList = [
2025-08-21 10:50:38 +08:00
{'icon': 'assets/images/share-wx.png', 'label': '好友'},
2025-07-21 15:46:30 +08:00
{'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<void> fetchVideoList() async {
if (isLoadingMore) return;
isLoadingMore = true;
try {
final res = await Http.post(VideoApi.vlogList, data: {
'current': page,
'size': pageSize,
});
final data = res['data'];
2025-08-21 17:50:34 +08:00
logger.d('关注用户的视频列表:$data');
2025-07-21 15:46:30 +08:00
if (data == null || (data is List && data.isEmpty)) {
return;
}
2025-08-21 17:50:34 +08:00
if (data['records'] is List) {
List videos = data['records'];
2025-08-21 10:50:38 +08:00
for (var item in videos) {
item['expanded'] = false;
}
2025-07-21 15:46:30 +08:00
setState(() {
if (page == 1) {
// 初始化
videoList = videos;
} else {
videoList.addAll(videos);
}
});
// 处理完成后
if (videos.isNotEmpty) {
page++;
}
2025-08-25 18:07:40 +08:00
logger.i('视频数据列表------------------->$videoList');
2025-07-21 15:46:30 +08:00
// 初始化播放器
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
}
}
2025-08-22 18:24:52 +08:00
// 取消喜欢视频
Future<void> doUnLikeVideo(item) async {
logger.d('点击了点赞按钮$item');
try {
final res = await Http.post(VideoApi.unlike, data:{
'vlogId': item['id']
});
final resCode = res['code'];
if (resCode == 200) {
2025-08-22 18:29:33 +08:00
item['doILikeThisVlog'] = !item['doILikeThisVlog'];
2025-08-22 18:24:52 +08:00
}
} catch (e) {
logger.i('点击取消喜欢按钮报错: $e');
}
}
// 点击喜欢视频
Future<void> doLikeVideo(item) async {
try {
final res = await Http.post(VideoApi.like, data:{
'vlogId': item['id']
});
final resCode = res['code'];
if (resCode == 200) {
2025-08-22 18:29:33 +08:00
item['doILikeThisVlog'] = !item['doILikeThisVlog'];
2025-08-22 18:24:52 +08:00
}
logger.i('点赞返回信息----------->: $res');
} catch (e) {
logger.i('点击喜欢按钮报错: $e');
}
}
// 评论弹框
void handleComment(index) {
2025-08-25 18:07:40 +08:00
final videoId = videoList[index]['id'];
2025-08-22 18:24:52 +08:00
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) {
2025-08-25 18:07:40 +08:00
return CommentBottomSheet(
videoId: videoId,
onCommentCountChanged: (newCount) {
// 更新对应视频的评论数量
setState(() {
if (index < videoList.length) {
videoList[index]['commentsCounts'] = newCount;
}
});
}
);
2025-08-22 18:24:52 +08:00
},
);
}
2025-07-21 15:46:30 +08:00
// 分享弹框
void handleShare(index) {
2025-08-21 10:50:38 +08:00
if (chatController.chatList.isNotEmpty) {
chatController.getConversationList();
}
2025-07-21 15:46:30 +08:00
showModalBottomSheet(
backgroundColor: Colors.white,
2025-08-21 10:50:38 +08:00
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)),
),
2025-07-21 15:46:30 +08:00
clipBehavior: Clip.antiAlias,
context: context,
2025-08-21 10:50:38 +08:00
isScrollControlled: true,
2025-07-21 15:46:30 +08:00
builder: (context) {
return Material(
color: Colors.white,
2025-08-21 10:50:38 +08:00
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
2025-07-21 15:46:30 +08:00
child: Column(
2025-08-21 10:50:38 +08:00
mainAxisSize: MainAxisSize.min,
2025-07-21 15:46:30 +08:00
children: [
2025-08-21 10:50:38 +08:00
// 分享列表
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),
2025-07-21 15:46:30 +08:00
child: Column(
2025-08-21 10:50:38 +08:00
mainAxisSize: MainAxisSize.min,
2025-07-21 15:46:30 +08:00
children: [
Image.asset('${shareList[index]['icon']}', width: 48.0),
2025-08-21 10:50:38 +08:00
SizedBox(height: 5),
2025-07-21 15:46:30 +08:00
Text(
'${shareList[index]['label']}',
style: TextStyle(fontSize: 12.0),
2025-08-21 10:50:38 +08:00
overflow: TextOverflow.ellipsis,
),
2025-07-21 15:46:30 +08:00
],
),
2025-08-21 10:50:38 +08:00
),
);
},
),
),
// 会话列表
2025-08-22 18:24:52 +08:00
SizedBox(
height: 110,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).length,
padding: 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: EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NetworkOrAssetImage(
imageUrl: filteredList[index].faceUrl,
width: 48.0,
height: 48.0,
),
SizedBox(height: 5),
Text(
'${filteredList[index].conversation.showName}',
style: TextStyle(fontSize: 12.0),
overflow: TextOverflow.ellipsis,
),
],
2025-08-21 10:50:38 +08:00
),
2025-08-22 18:24:52 +08:00
),
);
},
),
),
2025-08-21 10:50:38 +08:00
// 取消按钮
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),
),
2025-07-21 15:46:30 +08:00
),
),
),
],
),
),
);
},
);
}
2025-08-21 10:50:38 +08:00
void handleShareClick(int index) {
print("分享项 $index 被点击");
2025-08-26 15:22:16 +08:00
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}');
2025-08-21 10:50:38 +08:00
} else if (index == 2) {
2025-08-26 15:22:16 +08:00
// 复制链接到剪切板
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");
2025-08-21 10:50:38 +08:00
}
}
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);
}
}
2025-07-21 15:46:30 +08:00
@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) {
2025-08-22 18:24:52 +08:00
await fetchVideoList();
2025-07-21 15:46:30 +08:00
}
},
itemCount: videoList.length,
itemBuilder: (context, index) {
final videoWidth = videoList[index]['width'] ?? 1;
2025-08-22 18:24:52 +08:00
final videoHeight = videoList[index]['height'] ?? 1;
2025-07-21 15:46:30 +08:00
final isHorizontal = videoWidth > videoHeight;
2025-08-22 18:24:52 +08:00
final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList();
2025-07-21 15:46:30 +08:00
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,
2025-08-21 10:50:38 +08:00
child: GestureDetector(
2025-08-25 18:07:40 +08:00
onTap: ()async {
2025-08-21 10:50:38 +08:00
player.pause();
2025-08-25 18:07:40 +08:00
// 跳转到 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'];
};
2025-08-21 10:50:38 +08:00
},
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(
2025-08-22 18:24:52 +08:00
imageUrl: videoList[index]['commentUserFace'],
2025-08-21 10:50:38 +08:00
),
),
2025-07-21 15:46:30 +08:00
),
),
),
),
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,
),
),
2025-08-25 18:07:40 +08:00
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'];
});
}
}
2025-07-21 15:46:30 +08:00
},
),
),
],
),
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: () {
2025-08-22 18:24:52 +08:00
logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}');
if(videoList[index]['doILikeThisVlog'] == true){
logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}');
doUnLikeVideo(videoList[index]);
}else{
doLikeVideo(videoList[index]);
}
2025-07-21 15:46:30 +08:00
},
),
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);
},
),
2025-08-21 10:50:38 +08:00
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: () {
// 举报
},
),
2025-07-21 15:46:30 +08:00
],
),
),
Positioned(
2025-08-21 10:50:38 +08:00
bottom: 15.0,
left: 10.0,
right: 80.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
2025-08-22 18:24:52 +08:00
'@${videoList[videoModuleController.videoPlayIndex.value]['commentUserNickname'] ?? '未知'}',
2025-08-21 10:50:38 +08:00
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,
),
),
),
),
],
);
},
),
],
)),
2025-07-21 15:46:30 +08:00
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,
2025-08-22 18:24:52 +08:00
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0),
overlayShape: RoundSliderOverlayShape(overlayRadius: 0),
inactiveTrackColor: Colors.white24,
activeTrackColor: Colors.white,
thumbColor: Colors.white,
overlayColor: Colors.transparent,
2025-07-21 15:46:30 +08:00
),
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)),
],
),
)),
),
],
);
},
),
],
),
),
],
),
);
}
2025-08-22 18:24:52 +08:00
}