import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; import 'package:flutter_kinetra/kt_utils/kt_utils.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:video_player/video_player.dart'; import 'package:visibility_detector/visibility_detector.dart'; import '../../kt_widgets/kt_network_image.dart'; import '../../kt_widgets/kt_status_widget.dart'; import '../../kt_widgets/kt_video_progress_bar.dart'; import 'logic.dart'; class VideoPlayPage extends StatefulWidget { const VideoPlayPage({super.key}); @override State createState() => _VideoPlayPageState(); } class _VideoPlayPageState extends State with SingleTickerProviderStateMixin { final logic = Get.put(VideoPlayLogic()); final state = Get.find().state; @override void initState() { state.imageUrl = Get.arguments['imageUrl'] ?? ''; super.initState(); } // 选择倍速 void _selectSpeed(double speed) { state.currentSpeed = speed; logic.controllers[logic.currentIndex]?.setPlaybackSpeed(speed); } bool get isPause => !((logic.controllers[logic.currentIndex]?.value.isPlaying ?? true) || !(logic.controllers[logic.currentIndex]?.value.isBuffering ?? true)); bool get isAllOver => logic.currentIndex == state.episodeList.length - 1 && (logic.controllers[logic.currentIndex]?.value.isCompleted ?? false); @override Widget build(BuildContext context) { return PopScope( canPop: true, onPopInvokedWithResult: (didPop, result) async { // if (logic.controllers.isNotEmpty) { // logic.uploadHistorySeconds(logic.controllers[logic.currentIndex]?.value.position.inMilliseconds ?? 0); // } if (didPop) return; }, child: Scaffold( backgroundColor: Colors.white, body: GetBuilder( assignId: true, builder: (ctrl) { if (state.loadStatus == KtLoadStatusType.loadFailed) { return SizedBox( width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: EdgeInsets.only( top: ScreenUtil().statusBarHeight, ), child: IconButton( onPressed: () => Get.back(), icon: Icon(Icons.arrow_back_outlined, size: 28), ), ), SizedBox(height: 100.w), KtStatusWidget( type: KtErrorStatusType.loadFailed, onPressed: logic.fetchData, ), ], ), ); } return Stack( children: [ KtNetworkImage( imageUrl: state.imageUrl, width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, placeholder: Image.asset( 'bg2.png'.ktIcon, width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, fit: BoxFit.cover, ), ), BackdropFilter( filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), child: Container( color: Colors.black.withAlpha(30), width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, child: Center(child: CircularProgressIndicator()), ), ), // if (state.video == null) Center(child: CircularProgressIndicator(color: DrColors.mainColor)), PageView.builder( controller: logic.pageController, scrollDirection: Axis.vertical, onPageChanged: (index) => logic.onPageChanged(index, type: 1), itemCount: state.episodeList.length, itemBuilder: (context, index) => _videoPlayWidget(index), ), ], ); }, ), ), ); } Widget _videoPlayWidget(int index) { if (logic.controllers.isEmpty) return Container(); final controller = logic.controllers[index]; // final episode = state.episodeList[index]; return Stack( children: [ // 视频播放器 if (controller != null && controller.value.isInitialized && !isAllOver) GestureDetector( onTap: () { // if (episode.isLock == true) return; controller.value.isPlaying ? controller.pause() : controller.play(); setState(() {}); }, child: Stack( alignment: Alignment.center, children: [ Positioned.fill( child: FittedBox( fit: BoxFit.cover, child: VisibilityDetector( key: Key('short-video-$index'), onVisibilityChanged: (VisibilityInfo info) { var visiblePercentage = info.visibleFraction * 100; if (visiblePercentage > 20) { // if (episode.isLock == true) return; controller.play(); } else { controller.pause(); } logic.update(); }, child: SizedBox( width: controller.value.size.width, height: controller.value.size.height, child: VideoPlayer(controller), ), ), ), ), // if (logic.loadStatusType == LoadStatusType.loading) // Center(child: CircularProgressIndicator(color: DrColors.mainColor)), if (!controller.value.isPlaying) // if (!controller.value.isPlaying && logic.loadStatusType == LoadStatusType.loadSuccess) Center( child: Image.asset( 'ic_play.png'.ktIcon, width: 62.w, height: 62.w, ), ), ], ), ) else Stack( children: [ KtNetworkImage( width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, imageUrl: state.imageUrl, placeholder: Image.asset( 'bg2.png'.ktIcon, width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, fit: BoxFit.cover, ), ), if (!isAllOver) Center(child: CircularProgressIndicator()), ], ), // if (episode.isLock == true) // Container( // width: ScreenUtil().screenWidth, // height: ScreenUtil().screenHeight, // color: Colors.black.withValues(alpha: .75), // child: Center( // child: GestureDetector( // onTap: () { // EasyThrottle.throttle( // 'unlock', // Duration(seconds: 2), // () => logic.buyVideo( // episode.id!, // episode.coins ?? 0, // toRecharge: true, // ), // ); // }, // child: Container( // width: 260.w, // padding: EdgeInsets.symmetric(vertical: 17.w), // decoration: MyStyles.mainBorder(width: 1, radius: 50), // child: Row( // mainAxisAlignment: MainAxisAlignment.center, // children: [ // Image.asset('ic_unlock_lock.png'.ktIcon, width: 20.w), // SizedBox(width: 3.w), // Text( // state.curUnlock == index // ? 'Unlocking costs ${episode.coins ?? 0} coins' // : 'Prev.locked', // style: TextStyle( // fontSize: 14.sp, // color: DrColors.mainWhite, // ), // ), // ], // ), // ), // ), // ), // ), // 顶部返回 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), ], ), ), 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, ), ), ), 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, ), ), ], ), ), ), 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, ), ], ), ); }, ), ], ), ], ), ), ), ], ); } showFeatureDialog() { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => StatefulBuilder( builder: (context, dialogState) { return Container( height: 245.w, padding: EdgeInsets.symmetric(horizontal: 16.w), decoration: BoxDecoration( borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)), image: DecorationImage( image: AssetImage('ic_speed_bg.png'.ktIcon), fit: BoxFit.fill, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 36.w), GestureDetector( onTap: () { Get.back(); showSpeedSelDialog(); }, child: Container( padding: EdgeInsets.symmetric( horizontal: 12.w, vertical: 10.w, ), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Color(0xFF03011B).withValues(alpha: .4), Color(0xFF00000B).withValues(alpha: .4), ], ), borderRadius: BorderRadius.circular(8.w), ), child: Row( children: [ Container( margin: EdgeInsets.only(right: 5.w), child: Image.asset( 'ic_cur_speed.png'.ktIcon, width: 24.w, ), ), Text( 'Playback Speed', style: TextStyle( fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.w500, ), ), const Spacer(), Text( '${state.currentSpeed}x', style: TextStyle( fontSize: 12.sp, color: Colors.white, ), ), Container( margin: EdgeInsets.only(left: 9.w), child: Image.asset( 'ic_right_grey.png'.ktIcon, width: 8.w, ), ), ], ), ), ), SizedBox(height: 18.w), GestureDetector( onTap: () { Get.back(); }, child: Container( padding: EdgeInsets.symmetric( horizontal: 12.w, vertical: 10.w, ), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Color(0xFF03011B).withValues(alpha: .4), Color(0xFF00000B).withValues(alpha: .4), ], ), borderRadius: BorderRadius.circular(8.w), ), child: Row( children: [ Container( margin: EdgeInsets.only(right: 5.w), child: Image.asset( 'ic_video_feedback.png'.ktIcon, width: 16.w, ), ), Text( 'Feedback', style: TextStyle( fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.w500, ), ), const Spacer(), Container( margin: EdgeInsets.only(left: 9.w), child: Image.asset( 'ic_right_grey.png'.ktIcon, width: 8.w, ), ), ], ), ), ), ], ), ); }, ), ); } showEpSelDialog() { return Get.bottomSheet( Container( height: 550.w, padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), decoration: BoxDecoration( borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)), image: DecorationImage( image: AssetImage('ep_sel_bg.png'.ktIcon), fit: BoxFit.fill, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: EdgeInsets.only(top: 80.w), child: Row( children: [ Image.asset('ip.png'.ktIcon, width: 38.w), SizedBox(width: 6.w), Container( padding: EdgeInsets.symmetric( vertical: 9.w, horizontal: 14.w, ), decoration: BoxDecoration( color: Color(0xFF1E1E20), borderRadius: BorderRadius.circular(100), ), child: Row( children: [ Text( 'Select Episode', style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.w500, color: Color(0xFFA7F62F), ), ), Text( '(All ${state.video?.shortPlayInfo?.episodeTotal} episodes)', style: TextStyle( fontSize: 12.sp, color: Color(0xFFA7F62F), ), ), ], ), ), ], ), ), Container( margin: EdgeInsets.only(top: 16.w), child: Row( children: [ KtNetworkImage( imageUrl: state.video?.shortPlayInfo?.imageUrl ?? '', width: 64.w, height: 85.w, borderRadius: BorderRadius.circular(6.w), ), SizedBox(width: 10.w), SizedBox( width: ScreenUtil().screenWidth - 105.w, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( state.video?.shortPlayInfo?.name ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14.sp, color: Color(0xFF1E1E20), fontWeight: FontWeight.w500, ), ), SizedBox(height: 7.w), Text( state.video?.shortPlayInfo?.category?.first ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12.sp, color: Color(0xFFA7F62F), ), ), SizedBox(height: 9.w), if (!KtUtils.isEmpty( state.video?.shortPlayInfo?.description, )) SizedBox( height: 40.w, child: Text( state.video?.shortPlayInfo?.description ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 10.sp, color: Color(0xFF5E5E5E), ), ), ), ], ), ), ], ), ), SizedBox(height: 19.w), Expanded( child: GridView.builder( padding: EdgeInsets.only(bottom: 16.w), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, mainAxisSpacing: 10.w, crossAxisSpacing: 10.w, childAspectRatio: 61 / 50, ), itemCount: state.episodeList.length, itemBuilder: (context, index) { bool isCurrent = index == logic.currentIndex; return GestureDetector( onTap: () { Get.back(); // bool? beforeEpUnlock = index - 2 <= 0 ? false : state.episodeList[index - 2].isLock; // if (beforeEpUnlock ?? false) { // ToastUtils.show('Unable to unlock episodes'); // return; // } logic.onPageChanged(index, isToggle: true); }, child: Stack( children: [ Container( decoration: BoxDecoration( color: isCurrent ? Color(0xFFA7F62F) : Colors.white, borderRadius: BorderRadius.circular(10.w), border: isCurrent ? Border.all( color: Color(0xFF1E1E20), width: 1.w, ) : null, ), child: Center( child: Text( '${index + 1}', style: TextStyle( color: Color(0xFF1E1E20), fontSize: 14.sp, fontWeight: FontWeight.w500, ), ), ), ), // if (state.episodeList[episode - 1].isLock == true) // Positioned( // right: 0, // top: 0, // child: Container( // width: 20.w, // height: 12.w, // decoration: BoxDecoration( // color: ColorResource.mainYellow, // borderRadius: BorderRadius.only( // topRight: Radius.circular(6.w), // bottomLeft: Radius.circular(6.w), // ), // ), // child: Image.asset( // 'ic_video_lock.png'.icon, // width: 10.w, // height: 10.w, // ), // ), // ), ], ), ); }, ), ), ], ), ), isScrollControlled: true, ); } showSpeedSelDialog() { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => StatefulBuilder( builder: (context, dialogState) { return Container( height: 375.w, padding: EdgeInsets.symmetric(horizontal: 15.w), decoration: BoxDecoration( borderRadius: BorderRadius.vertical(top: Radius.circular(12.w)), image: DecorationImage( image: AssetImage('ic_speed_bg.png'.ktIcon), fit: BoxFit.fill, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 44.w), Row( children: [ Container( margin: EdgeInsets.only(right: 5.w), child: Image.asset( 'ic_cur_speed.png'.ktIcon, width: 24.w, ), ), Text( 'Playback Speed', style: TextStyle( fontSize: 18.sp, color: Colors.white, fontWeight: FontWeight.w500, ), ), SizedBox(width: 12.w), Text( '· ${state.currentSpeed}X', style: TextStyle( fontSize: 16.sp, color: Color(0xFF36F2FF), fontWeight: FontWeight.w500, ), ), ], ), SizedBox(height: 15.w), Expanded( child: ListView.separated( shrinkWrap: true, itemBuilder: (context, index) => GestureDetector( onTap: () { _selectSpeed(state.speedList[index]); Get.back(); }, child: Container( padding: EdgeInsets.all(12.w), alignment: Alignment.centerLeft, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.w), color: Colors.white.withValues(alpha: .16), border: state.currentSpeed == state.speedList[index] ? Border.all(width: 2.w, color: Color(0xFF36F2FF)) : null, ), child: Text( '${state.speedList[index]}x', style: TextStyle( fontSize: 14.sp, color: Colors.white, ), ), ), ), separatorBuilder: (_, __) => SizedBox(height: 10.w), itemCount: state.speedList.length, ), ), ], ), ); }, ), ); } }