diff --git a/assets/home_bottom_video.png b/assets/home_bottom_video.png new file mode 100644 index 0000000..d81441a Binary files /dev/null and b/assets/home_bottom_video.png differ diff --git a/assets/video_recommend_bg.png b/assets/video_recommend_bg.png new file mode 100644 index 0000000..35e7a25 Binary files /dev/null and b/assets/video_recommend_bg.png differ diff --git a/assets/video_recommend_btn.png b/assets/video_recommend_btn.png new file mode 100644 index 0000000..a327b74 Binary files /dev/null and b/assets/video_recommend_btn.png differ diff --git a/assets/video_recommend_play.png b/assets/video_recommend_play.png new file mode 100644 index 0000000..a9a79a1 Binary files /dev/null and b/assets/video_recommend_play.png differ diff --git a/lib/kt_pages/kt_home/view.dart b/lib/kt_pages/kt_home/view.dart index dff0739..cbb9a0a 100644 --- a/lib/kt_pages/kt_home/view.dart +++ b/lib/kt_pages/kt_home/view.dart @@ -66,6 +66,87 @@ class _KtHomePageState extends State ), ), ), + // 悬浮小视频卡片 + if (state.showVideo && state.curVideo != null) + Positioned( + bottom: 2.w, + right: 15.w, + child: GestureDetector( + onTap: () => Get.toNamed( + KtRoutes.shortVideo, + arguments: { + 'shortPlayId': state.curVideo?.shortPlayId, + 'imageUrl': state.curVideo?.imageUrl ?? '', + }, + ), + child: Container( + width: 345.w, + height: 40.w, + padding: EdgeInsets.only(right: 10.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20.w), + ), + child: Row( + children: [ + Container( + width: 40.w, + height: 40.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('home_bottom_video.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Center( + child: KtNetworkImage( + imageUrl: state.curVideo?.imageUrl ?? '', + width: 30.w, + height: 30.w, + borderRadius: BorderRadius.circular(15.w), + ), + ), + ), + SizedBox(width: 8.w), + Container( + constraints: BoxConstraints(maxWidth: 160.w), + child: Text( + state.curVideo?.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF1E1E20), + ), + ), + ), + SizedBox(width: 20.w), + Text( + 'Ep.${state.curVideo?.process}', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: Color(0xFF94949B), + ), + ), + const Spacer(), + Image.asset( + 'ic_home_play.png'.ktIcon, + width: 24.w, + height: 24.w, + ), + SizedBox(width: 14.w), + Image.asset( + 'ic_home_eposide.png'.ktIcon, + width: 24.w, + height: 24.w, + ), + ], + ), + ), + ), + ), ], ); }, @@ -1098,8 +1179,7 @@ class _KtHomePageState extends State height: 127.w, child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( video.name ?? '', diff --git a/lib/kt_pages/kt_main_page/view.dart b/lib/kt_pages/kt_main_page/view.dart index 5fcfd71..b0f7609 100644 --- a/lib/kt_pages/kt_main_page/view.dart +++ b/lib/kt_pages/kt_main_page/view.dart @@ -95,6 +95,7 @@ class _KtMainPageState extends State itemCount: _tabsTitle.length, ), bottomNavigationBar: BottomNavigationBar( + backgroundColor: Colors.white, selectedItemColor: Color(0xFF1E1E20), selectedLabelStyle: TextStyle( fontSize: 10.sp, @@ -102,7 +103,7 @@ class _KtMainPageState extends State ), unselectedLabelStyle: TextStyle( fontSize: 10.sp, - color: Color(0xFF95959C), + color: Color(0xFF94949B), fontWeight: FontWeight.w400, ), type: BottomNavigationBarType.fixed, diff --git a/lib/kt_pages/kt_short_video/logic.dart b/lib/kt_pages/kt_short_video/logic.dart index 68b3c93..b3577be 100644 --- a/lib/kt_pages/kt_short_video/logic.dart +++ b/lib/kt_pages/kt_short_video/logic.dart @@ -1,12 +1,15 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'dart:io'; - +import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:card_swiper/card_swiper.dart'; import 'package:flutter_kinetra/kt_pages/kt_mine/logic.dart'; import 'package:flutter_kinetra/kt_pages/kt_short_video/state.dart'; import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; import 'package:flutter_kinetra/kt_utils/kt_toast_utils.dart'; +import 'package:flutter_kinetra/kt_pages/kt_home/logic.dart'; +import 'package:flutter_kinetra/kt_pages/kt_my_list/logic.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:video_player/video_player.dart'; @@ -14,10 +17,13 @@ import 'package:video_player/video_player.dart'; import '../../dio_cilent/kt_apis.dart'; import '../../dio_cilent/kt_request.dart'; import '../../kt_model/kt_video_detail_bean.dart'; +import '../../kt_model/kt_short_video_bean.dart'; import '../../kt_utils/kt_device_info_utils.dart'; import '../../kt_widgets/kt_status_widget.dart'; import '../../kt_widgets/kt_store_widget.dart'; +import '../../kt_widgets/kt_dialog.dart'; import '../kt_mine/kt_store/logic.dart'; +import '../../kt_widgets/kt_network_image.dart'; class VideoPlayLogic extends GetxController { final state = VideoPlayState(); @@ -30,6 +36,9 @@ class VideoPlayLogic extends GetxController { bool _disposed = false; final userLogic = Get.put(KtMineLogic()); + final mylistLogic = Get.put(MyListLogic()); + final mylistState = Get.find().state; + @override void onInit() { super.onInit(); @@ -45,16 +54,44 @@ class VideoPlayLogic extends GetxController { state.imageUrl = Get.arguments['imageUrl'] ?? ''; state.activityId = Get.arguments['activityId']; state.isFromDiscover = Get.arguments['isFromDiscover'] ?? false; + startTimer(); fetchData(); + getRecommend(); + startVideoTimer(); } @override void onClose() { _disposed = true; clearCacheCtrl(); + state.timer?.cancel(); + state.videotimer?.cancel(); super.onClose(); } + // 推荐点击 + initData(shortPlayId, imageUrl) { + uploadHistorySeconds( + controllers[currentIndex]?.value.position.inMilliseconds ?? 0, + ); + Get.back(); + state.shortPlayId = shortPlayId; + state.videoId = 0; + state.imageUrl = imageUrl; + state.isRecommend = false; + + // pageController.dispose(); + currentIndex = 0; + pageController.jumpToPage(currentIndex); + // pageController = PageController(initialPage: currentIndex); + clearCacheCtrl(); + fetchData(); + state.recommendList.clear(); + getRecommend(); + startTimer(); + startVideoTimer(); + } + clearCacheCtrl() { for (var controller in controllers) { controller?.pause(); @@ -123,6 +160,37 @@ class VideoPlayLogic extends GetxController { } } + // 5s之后返回推荐 + void startTimer() { + state.timer = Timer(const Duration(seconds: 5), () { + state.isRecommend = true; + update(); + }); + } + + // 5s 之后隐藏控制器 + void startVideoTimer() { + state.videotimer?.cancel(); + state.videotimer = Timer(const Duration(seconds: 5), () { + state.isVideoctrHide = true; + update(); + }); + } + + // 获取推荐列表 + getRecommend() async { + ApiResponse res = await KtHttpClient().request( + KtApis.getDetailsRecommand, + method: HttpMethod.get, + ); + if (res.success) { + state.recommendList = [ + ...res.data['list'].map((item) => KtShortVideoBean.fromJson(item)), + ]; + update(); + } + } + void reportHistory() { if (currentIndex < 0 || currentIndex >= state.episodeList.length) return; Map params = { @@ -141,6 +209,19 @@ class VideoPlayLogic extends GetxController { KtHttpClient().request(KtApis.activeAfterWatchingVideo, data: params); } + void updateHomeVideo() { + final homeLogic = Get.put(KtHomeLogic()); + final homeState = Get.find().state; + int playTime = controllers[currentIndex]?.value.position.inSeconds ?? 0; + homeState.curVideo = KtShortVideoBean() + ..shortPlayId = state.shortPlayId + ..imageUrl = state.video?.shortPlayInfo?.imageUrl + ..name = state.video?.shortPlayInfo?.name + ..playTime = playTime + ..process = currentIndex + 1; + homeLogic.update(); + } + // 切换剧集时处理视频状态 Future onPageChanged( int index, { @@ -200,7 +281,7 @@ class VideoPlayLogic extends GetxController { } } controllers[index]?.setPlaybackSpeed(state.currentSpeed); - // updateHomeVideo(); + updateHomeVideo(); // print('----curIndex:$currentIndex'); // 预加载新的相邻视频,并释放多余控制器 _preloadAdjacentVideos(); @@ -241,7 +322,8 @@ class VideoPlayLogic extends GetxController { try { await controller.initialize(); - if (index == currentIndex && (episode.isLock == false || userLogic.state.userInfo.isVip == true)) { + if (index == currentIndex && + (episode.isLock == false || userLogic.state.userInfo.isVip == true)) { controller.play(); update(); } @@ -251,7 +333,7 @@ class VideoPlayLogic extends GetxController { if (currentIndex == index && !_disposed) update(); if (currentIndex == state.episodeList.length - 1 && (controllers.last?.value.isCompleted ?? false)) { - // showRecommendDialog(); + showRecommendDialog(); } if (controller.value.isCompleted && !controller.value.isBuffering) { onPageChanged(index + 1, isToggle: true); @@ -291,20 +373,21 @@ class VideoPlayLogic extends GetxController { Future likeVideo() async { if (state.video == null) return; - Map params = { "short_play_id": state.video?.shortPlayInfo?.shortPlayId, "video_id": state.episodeList[currentIndex].id, }; if (state.video?.shortPlayInfo?.isCollect ?? false) { - await KtHttpClient().request(KtApis.deleteFavoriteVideo, data: params); + // await KtHttpClient().request(KtApis.deleteFavoriteVideo, data: params); + cancelCollect(state.video!.shortPlayInfo!.shortPlayId!); } else { await KtHttpClient().request(KtApis.collectVideo, data: params); + state.video?.shortPlayInfo?.isCollect = + !(state.video?.shortPlayInfo?.isCollect ?? false); + update(); + mylistLogic.getCollectList(refresh: true); + mylistLogic.getHistoryList(refresh: true); } - - state.video?.shortPlayInfo?.isCollect = - !(state.video?.shortPlayInfo?.isCollect ?? false); - update(); } // 购买剧集 @@ -428,4 +511,202 @@ class VideoPlayLogic extends GetxController { ), ); } + + cancelCollect(num id) async { + Get.dialog( + KtDialog( + title: 'Remove from your list?', + subTitle: 'This drama will be removed from your saved list.', + rightBtnText: 'Remove', + rightBtnIcon: 'ic_dialog_delete.png', + rightBtnFunc: () async { + ApiResponse res = await KtHttpClient().request( + KtApis.deleteFavoriteVideo, + queryParameters: {'short_play_id': id}, + ); + if (res.success) { + state.video?.shortPlayInfo?.isCollect = + !(state.video?.shortPlayInfo?.isCollect ?? false); + update(); + // state.favoriteList.removeWhere((item) => id == item.shortPlayId); + mylistState.chestList.removeWhere((item) => id == item.shortPlayId); + mylistState.historyList + .firstWhereOrNull((item) => id == item.shortPlayId) + ?.isCollect = + 0; + mylistLogic.update(); + } + }, + ), + ); + } + + showRecommendDialog() { + EasyThrottle.throttle('show-recommend', Duration(seconds: 3), () async { + controllers[currentIndex]?.pause(); + Get.bottomSheet( + isScrollControlled: true, + isDismissible: false, + enableDrag: false, + Stack( + children: [ + Positioned( + top: ScreenUtil().statusBarHeight + 10.w, + left: 16.w, + child: GestureDetector( + onTap: () { + EasyThrottle.throttle( + 'back-recommend', + Duration(seconds: 3), + () async { + Get.back(); + Get.back(); + }, + ); + }, + child: Image.asset('ic_back_white.png'.ktIcon, width: 24.w), + ), + ), + Positioned( + bottom: 0, + left: 0, + child: Container( + height: 503.w, + width: ScreenUtil().screenWidth, + padding: EdgeInsets.fromLTRB(0.w, 75.w, 0.w, 20.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('video_recommend_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 15.w), + Image.asset('ip.png'.ktIcon, width: 38.w, height: 40.w), + SizedBox(width: 6.w), + Container( + height: 30.w, + padding: EdgeInsets.symmetric(horizontal: 14.w), + decoration: BoxDecoration( + color: Color(0xFF1E1E20), + borderRadius: BorderRadius.circular(15.w), + ), + child: Center( + child: Text( + 'More Drama Gold Below!', + style: TextStyle( + color: Color(0xFFA7F62F), + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + SizedBox( + height: 290.w, + child: Swiper( + layout: SwiperLayout.CUSTOM, + customLayoutOption: + CustomLayoutOption(startIndex: -1, stateCount: 3) + ..addRotate([-20.0 / 180, 0.0, 20.0 / 180]) + ..addTranslate([ + Offset(-220.w, 0), + Offset(0, 0), + Offset(220.w, 0), + ]), + itemWidth: 190.w, + itemHeight: 226.w, + itemBuilder: (context, index) { + final item = + state.recommendList[index % + state.recommendList.length]; + return GestureDetector( + onTap: () { + initData( + state.recommendList[index].shortPlayId ?? -1, + state.recommendList[index].imageUrl ?? '', + ); + }, + child: KtNetworkImage( + imageUrl: item.imageUrl ?? '', + width: 190.w, + height: 226.w, + borderRadius: BorderRadius.circular(20.w), + ), + ); + }, + itemCount: state.recommendList.length, + loop: true, + autoplay: true, + onIndexChanged: (index) => state.recommendIndex = index, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + initData( + state + .recommendList[state.recommendIndex] + .shortPlayId ?? + -1, + state + .recommendList[state.recommendIndex] + .imageUrl ?? + '', + ); + }, + child: Container( + width: 280.w, + height: 64.w, + padding: EdgeInsets.only(bottom: 16.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + 'video_recommend_btn.png'.ktIcon, + ), + fit: BoxFit.cover, + ), + ), + child: Container( + height: 48.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'video_recommend_play.png'.ktIcon, + width: 18.w, + height: 18.w, + ), + SizedBox(width: 4.w), + Text( + 'watch now', + style: TextStyle( + color: Colors.black, + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + }); + } } diff --git a/lib/kt_pages/kt_short_video/state.dart b/lib/kt_pages/kt_short_video/state.dart index e22ffd4..5f47b49 100644 --- a/lib/kt_pages/kt_short_video/state.dart +++ b/lib/kt_pages/kt_short_video/state.dart @@ -1,9 +1,12 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; import '../../kt_model/kt_video_detail_bean.dart'; import '../../kt_widgets/kt_status_widget.dart'; +import '../../kt_model/kt_short_video_bean.dart'; class VideoPlayState { String imageUrl = ''; - int shortPlayId = -1; + num shortPlayId = -1; num videoId = -1; int curUnlock = 999; int? activityId; @@ -15,4 +18,12 @@ class VideoPlayState { int currentVideoIndex = 0; double currentSpeed = 1.0; final List speedList = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0]; + List recommendList = []; + int recommendIndex = 0; + + bool isRecommend = false; + Timer? timer; + + bool isVideoctrHide = false; + Timer? videotimer; } diff --git a/lib/kt_pages/kt_short_video/view.dart b/lib/kt_pages/kt_short_video/view.dart index c840f57..5ccd9e5 100644 --- a/lib/kt_pages/kt_short_video/view.dart +++ b/lib/kt_pages/kt_short_video/view.dart @@ -32,6 +32,7 @@ class _VideoPlayPageState extends State @override void initState() { state.imageUrl = Get.arguments['imageUrl'] ?? ''; + // state.isFromRecommend = Get.arguments['isFromRecommend'] ?? false; super.initState(); } @@ -54,9 +55,16 @@ class _VideoPlayPageState extends State return PopScope( canPop: true, onPopInvokedWithResult: (didPop, result) async { - // if (logic.controllers.isNotEmpty) { - // logic.uploadHistorySeconds(logic.controllers[logic.currentIndex]?.value.position.inMilliseconds ?? 0); - // } + if (logic.controllers.isNotEmpty) { + logic.uploadHistorySeconds( + logic + .controllers[logic.currentIndex] + ?.value + .position + .inMilliseconds ?? + 0, + ); + } if (didPop) return; }, child: Scaffold( @@ -206,9 +214,22 @@ class _VideoPlayPageState extends State GestureDetector( onTap: () { if (episode.isLock == true) return; - (controller?.value.isPlaying ?? true) - ? controller?.pause() - : controller?.play(); + + if (controller?.value.isPlaying ?? true) { + if (state.isVideoctrHide) { + state.isVideoctrHide = false; + logic.startVideoTimer(); + } else { + controller?.pause(); + state.videotimer?.cancel(); + } + } else { + logic.startVideoTimer(); + controller?.play(); + } + // (controller?.value.isPlaying ?? true) + // ? controller?.pause() + // : controller?.play(); setState(() {}); }, child: Container( @@ -298,123 +319,126 @@ class _VideoPlayPageState extends State ), ], ), - // 顶部返回 - Positioned( - top: ScreenUtil().statusBarHeight + 10.w, - left: 16.w, - child: GestureDetector( - onTap: () { - // if (state.isFromRecommend || state.recommendList.isEmpty) { - Get.back(); - // } else { - // logic.showRecommendDialog(); - // } - }, - child: Image.asset('ic_back_white.png'.ktIcon, width: 24.w), - ), - ), - // 底部控制器 - Positioned( - bottom: 0, - left: 0.w, - child: Container( - width: ScreenUtil().screenWidth, - padding: EdgeInsets.fromLTRB(15.w, 40.w, 15.w, 40.w), - alignment: Alignment.bottomCenter, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xFF001D1F).withValues(alpha: 0), - Color(0xFF001D1F), - ], - ), + + if (!state.isVideoctrHide) ...[ + // 顶部返回 + Positioned( + top: ScreenUtil().statusBarHeight + 10.w, + left: 16.w, + child: GestureDetector( + onTap: () { + if (!state.isRecommend || state.recommendList.isEmpty) { + Get.back(); + } else { + logic.showRecommendDialog(); + } + }, + child: Image.asset('ic_back_white.png'.ktIcon, width: 24.w), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: ScreenUtil().screenWidth - 100.w, - child: Text( - state.video?.shortPlayInfo?.name ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 16.sp, - color: Colors.white, - fontWeight: FontWeight.w500, - ), - ), + ), + // 底部控制器 + Positioned( + bottom: 0, + left: 0.w, + child: Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.fromLTRB(15.w, 40.w, 15.w, 40.w), + alignment: Alignment.bottomCenter, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF001D1F).withValues(alpha: 0), + Color(0xFF001D1F), + ], ), - if (logic.controllers[index] != null) - CustomVideoProgressBar( - controller: logic.controllers[index]!, - width: ScreenUtil().screenWidth, - ), - SizedBox(height: 16.w), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: showEpSelDialog, - child: Container( - width: 300.w, - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 9.w, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50.w), - color: Colors.white.withValues(alpha: .1), - ), - child: Row( - children: [ - Image.asset('ic_ep.png'.ktIcon, width: 16.sp), - SizedBox(width: 6.w), - Text( - 'EP.${index + 1}', - style: TextStyle( - fontSize: 13.sp, - color: Colors.white, - ), - ), - const Spacer(), - Text( - 'All ${state.video?.shortPlayInfo?.episodeTotal ?? 0} Episodes', - style: TextStyle( - fontSize: 13.sp, - color: Colors.white, - ), - ), - ], - ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: ScreenUtil().screenWidth - 100.w, + child: Text( + state.video?.shortPlayInfo?.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + fontWeight: FontWeight.w500, ), ), - GetBuilder( - id: 'video-like', - builder: (c) { - return GestureDetector( - onTap: () => logic.likeVideo(), - child: Column( + ), + if (logic.controllers[index] != null) + CustomVideoProgressBar( + controller: logic.controllers[index]!, + width: ScreenUtil().screenWidth, + ), + SizedBox(height: 16.w), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: showEpSelDialog, + child: Container( + width: 300.w, + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 9.w, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50.w), + color: Colors.white.withValues(alpha: .1), + ), + child: Row( children: [ - Image.asset( - state.video?.shortPlayInfo?.isCollect == true - ? 'ic_collect_sel.png'.ktIcon - : 'ic_collect_unsel.png'.ktIcon, - width: 32.w, + Image.asset('ic_ep.png'.ktIcon, width: 16.sp), + SizedBox(width: 6.w), + Text( + 'EP.${index + 1}', + style: TextStyle( + fontSize: 13.sp, + color: Colors.white, + ), + ), + const Spacer(), + Text( + 'All ${state.video?.shortPlayInfo?.episodeTotal ?? 0} Episodes', + style: TextStyle( + fontSize: 13.sp, + color: Colors.white, + ), ), ], ), - ); - }, - ), - ], - ), - ], + ), + ), + GetBuilder( + id: 'video-like', + builder: (c) { + return GestureDetector( + onTap: () => logic.likeVideo(), + child: Column( + children: [ + Image.asset( + state.video?.shortPlayInfo?.isCollect == true + ? 'ic_collect_sel.png'.ktIcon + : 'ic_collect_unsel.png'.ktIcon, + width: 32.w, + ), + ], + ), + ); + }, + ), + ], + ), + ], + ), ), ), - ), + ], ], ); }