flutter/lib/pages/video/module/recommend.dart
2025-07-21 15:46:30 +08:00

826 lines
35 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 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/im_core.dart';
import 'package:loopin/api/video_api.dart';
import 'package:loopin/service/http.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 '../../../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();
}
class _RecommendModuleState extends State<RecommendModule> {
VideoModuleController videoModuleController = Get.put(VideoModuleController());
// class _RecommendModuleState extends State<RecommendModule> with AutomaticKeepAliveClientMixin {
// @override
// bool get wantKeepAlive => true;
// VideoModuleController videoModuleController = Get.find<VideoModuleController>();
// 分页内容
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 = [];
// 评论数据
List commentList = [
{'avatar': 'assets/images/avatar/img01.jpg', 'name': 'Alice', 'desc': '用汗水浇灌希望,让努力铸就辉煌,你付出的每一刻,都是在靠近成功的彼岸。'},
{'avatar': 'assets/images/avatar/img02.jpg', 'name': '悟空', 'desc': '黑暗遮不住破晓的曙光,困境困不住奋进的脚步,勇往直前,你定能冲破阴霾。'},
{'avatar': 'assets/images/avatar/img03.jpg', 'name': '木棉花', 'desc': '每一次跌倒都是为了下一次更有力地跃起,别放弃~'},
{'avatar': 'assets/images/avatar/img04.jpg', 'name': '狗仔', 'desc': '人生没有白走的路,每一步都算数,那些辛苦的过往,会在未来化作最美的勋章。'},
{'avatar': 'assets/images/avatar/img05.jpg', 'name': '向日葵', 'desc': '以梦为马,不负韶华,握紧手中的笔,书写属于自己的热血传奇,让青春绽放光芒。'},
{'avatar': 'assets/images/avatar/img06.jpg', 'name': '健身女神', 'desc': '哪怕身处谷底,只要抬头仰望,便能看见漫天繁星,心怀希望,就能找到出路,奔赴美好。'},
];
// 分享列表
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<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'];
if (data == null || (data is List && data.isEmpty)) {
// MyDialog.toast('没有更多了', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
return;
}
if (data['rows'] is List) {
List videos = data['rows'];
// for (var item in videos) {
// print("喜欢:${item['likeCounts']}");
// print("评论:${item['commentsCounts']}");
// }
setState(() {
if (page == 1) {
// 初始化
videoList = videos;
} else {
videoList.addAll(videos);
}
});
// 处理完成后
if (videos.isNotEmpty) {
page++;
}
logger.i('获取新的视频数据了');
// 初始化播放器
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
}
}
// 评论弹框
void handleComment(index) {
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 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: () {
Get.back();
},
),
],
),
Text(
'168条评论',
style: TextStyle(fontSize: 12.0, fontWeight: FontWeight.w600),
)
],
),
),
Expanded(
child: ScrollConfiguration(
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
child: ListView.builder(
physics: BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: commentList.length,
itemBuilder: (context, index) {
return ListTile(
isThreeLine: true,
leading: ClipRRect(
borderRadius: BorderRadius.circular(50.0),
child: Image.asset(
'${commentList[index]['avatar']}',
width: 30.0,
fit: BoxFit.contain,
),
),
title: Row(
children: [
Expanded(
child: Text(
'${commentList[index]['name']}',
style: TextStyle(
color: Colors.grey,
fontSize: 12.0,
),
),
),
Row(
children: [
Icon(
Icons.favorite_border_outlined,
color: Colors.black54,
size: 16.0,
),
Text(
'99',
style: TextStyle(color: Colors.black54, fontSize: 12.0),
),
],
),
SizedBox(
width: 20.0,
),
Row(
children: [
Icon(
Icons.heart_broken_outlined,
color: Colors.black54,
size: 16.0,
),
],
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.symmetric(vertical: 5.0),
child: Text(
'${commentList[index]['desc']}',
style: TextStyle(
fontSize: 14.0,
),
),
),
Row(
children: [
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(
'12回复',
style: TextStyle(fontSize: 12.0),
),
Icon(
Icons.arrow_forward_ios,
size: 10.0,
)
]),
),
Text(
'01-15 · 浙江',
style: TextStyle(color: Colors.grey, fontSize: 12.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(
'说点什么...',
style: TextStyle(color: Colors.black54, fontSize: 14.0),
),
],
),
),
onTap: () {
navigator?.push(FadeRoute(child: PopupReply(
onChanged: (value) {
debugPrint('评论内容: $value');
},
)));
},
),
],
),
);
},
);
}
// 分享弹框
void handleShare(index) {
showModalBottomSheet(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))),
clipBehavior: Clip.antiAlias,
context: context,
builder: (context) {
return Material(
color: Colors.white,
child: SizedBox(
height: 170,
width: double.infinity,
child: Column(
children: [
Expanded(
child: ScrollConfiguration(
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
child: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 10.0),
itemCount: shareList.length,
itemBuilder: (context, index) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
spacing: 5.0,
children: [
Image.asset('${shareList[index]['icon']}', width: 48.0),
Text(
'${shareList[index]['label']}',
style: TextStyle(fontSize: 12.0),
)
],
),
);
},
),
),
),
InkWell(
child: Container(
alignment: Alignment.center,
width: double.infinity,
height: 50.0,
color: Colors.grey[50],
child: Text(
'取消',
style: TextStyle(color: Colors.black87),
),
),
onTap: () {
Get.back();
},
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
// super.build(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(() {
// 重置slider参数
sliderValue = 0.0;
sliderDraging = false;
position = Duration.zero;
duration = Duration.zero;
});
player.stop();
// await player.open(Media(videoList[index]['src']));
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; // 防止除以0
final isHorizontal = videoWidth > videoHeight;
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,
),
),
// 封面图播放后透明度渐变为0而不是直接隐藏
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: 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:
Image.network(videoList[index]['vlogerFace'] ?? 'https://wuzhongjie.com.cn/download/logo.png', fit: BoxFit.cover),
),
),
),
),
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: () {
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: () {
setState(() {
videoList[index]['doILikeThisVlog'] = !videoList[index]['doILikeThisVlog'];
});
},
),
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);
},
),
// Column(
// children: [
// SvgPicture.asset(
// 'assets/images/svg/favor.svg',
// colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
// height: 40.0,
// width: 40.0,
// ),
// Text(
// '${videoList[index]['starNum']}',
// style: TextStyle(color: Colors.white, fontSize: 12.0),
// ),
// ],
// ),
GestureDetector(
child: Column(
children: [
SvgPicture.asset(
'assets/images/svg/share.svg',
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
height: 40.0,
width: 40.0,
),
// Text(
// '${videoList[index]['shareNum']}',
// style: TextStyle(color: Colors.white, fontSize: 12.0),
// ),
],
),
onTap: () {
handleShare(index);
},
),
],
),
),
// 底部信息区域
Positioned(
bottom: 15.0,
left: 10.0,
right: 80.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 5.0,
children: [
Text(
'@${videoList[index]['vlogerName'] ?? '未知'}',
style: TextStyle(color: Colors.white, fontSize: 16.0),
),
Text(
'${videoList[index]['content'] ?? '未知'}',
style: TextStyle(color: Colors.white, fontSize: 14.0),
),
],
),
),
// mini播放进度条
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), // 调整滑块的大小
// trackShape: RectangularSliderTrackShape(), // 使用矩形轨道形状
overlayShape: RoundSliderOverlayShape(overlayRadius: 0), // 去掉Slider默认上下边距间隙
inactiveTrackColor: Colors.white24, // 设置非活动进度条的颜色
activeTrackColor: Colors.white, // 设置活动进度条的颜色
thumbColor: Colors.white, // 设置滑块的颜色
overlayColor: Colors.transparent, // 设置滑块覆盖层的颜色
),
child: Slider(
value: sliderValue,
onChanged: (value) async {
// debugPrint('当前视频播放时间$value');
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)),
],
),
)),
),
],
);
},
),
/// 固定层
// 红包广告,先不做
// Ads(),
],
),
),
],
),
);
}
}