/// 精选推荐模块 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 createState() => _RecommendModuleState(); } class _RecommendModuleState extends State { VideoModuleController videoModuleController = Get.put(VideoModuleController()); // class _RecommendModuleState extends State with AutomaticKeepAliveClientMixin { // @override // bool get wantKeepAlive => true; // VideoModuleController videoModuleController = Get.find(); // 分页内容 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 = []; // 评论数据 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 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(), ], ), ), ], ), ); } }