import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:video_player/video_player.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; class ChallengeVideoController extends GetxController { late VideoPlayerController videoController; var isInitialized = false.obs; var isLoading = true.obs; var hasError = false.obs; var errorMessage = ''.obs; var showControls = true.obs; void initialize(String url) async { try { isLoading.value = true; hasError.value = false; videoController = VideoPlayerController.networkUrl(Uri.parse(url)); await videoController.initialize(); isInitialized.value = true; isLoading.value = false; videoController.play(); // Rebuild UI whenever video state changes videoController.addListener(() => update()); } catch (e) { hasError.value = true; isLoading.value = false; errorMessage.value = e.toString(); } } void toggleControls() => showControls.toggle(); void togglePlayPause() { videoController.value.isPlaying ? videoController.pause() : videoController.play(); update(); } // void seekForward() { // final pos = videoController.value.position; // final dur = videoController.value.duration; // if (pos + Duration(seconds: 10) < dur) { // videoController.seekTo(pos + Duration(seconds: 10)); // } // } // void seekBackward() { // final pos = videoController.value.position; // videoController.seekTo(pos - Duration(seconds: 10)); // } void seekForward() { final wasPlaying = videoController.value.isPlaying; final position = videoController.value.position; final duration = videoController.value.duration; final target = position + Duration(seconds: 10); final clampedTarget = target > duration ? duration : target; videoController.seekTo(clampedTarget).then((_) { if (wasPlaying) { Future.delayed(Duration(milliseconds: 100), () { if (!videoController.value.isPlaying) { videoController.play(); } }); } }); } void seekBackward() { final wasPlaying = videoController.value.isPlaying; final position = videoController.value.position; final target = position - Duration(seconds: 10); final clampedTarget = target < Duration.zero ? Duration.zero : target; videoController.seekTo(clampedTarget).then((_) { if (wasPlaying) { Future.delayed(Duration(milliseconds: 100), () { if (!videoController.value.isPlaying) { videoController.play(); } }); } }); } void toggleMute() { final isMuted = videoController.value.volume == 0; videoController.setVolume(isMuted ? 1.0 : 0.0); update(); } bool get isMuted => videoController.value.volume == 0; void disposeController() { videoController.pause(); videoController.dispose(); } String formatDuration(Duration duration) { String twoDigits(int n) => n.toString().padLeft(2, '0'); final minutes = twoDigits(duration.inMinutes.remainder(60)); final seconds = twoDigits(duration.inSeconds.remainder(60)); final hours = duration.inHours; if (hours > 0) { return '${twoDigits(hours)}:$minutes:$seconds'; } else { return '$minutes:$seconds'; } } } class ChallengeVideoPopup extends StatefulWidget { final String videoUrl; final String title; const ChallengeVideoPopup({ super.key, required this.videoUrl, required this.title, }); @override State createState() => _ChallengeVideoPopupState(); } class _ChallengeVideoPopupState extends State { final controller = Get.put(ChallengeVideoController()); @override void initState() { super.initState(); controller.initialize(widget.videoUrl); } @override void dispose() { controller.disposeController(); Get.delete(); super.dispose(); } @override Widget build(BuildContext context) { return Dialog( backgroundColor: Colors.transparent, insetPadding: EdgeInsets.all(16.w), child: Container( decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(16.r), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildHeader(), Container( height: 250.h, width: double.infinity, padding: EdgeInsets.all(16.w), child: Obx(() { if (controller.isLoading.value) { return Center( child: CircularProgressIndicator(color: Colors.white), ); } if (controller.hasError.value) { return Center( child: Text( controller.errorMessage.value, style: TextStyle(color: Colors.red), ), ); } if (!controller.isInitialized.value) { return Center( child: Text( "Initializing...", style: TextStyle(color: Colors.white), ), ); } return GestureDetector( onTap: controller.toggleControls, child: Stack( alignment: Alignment.center, children: [ AspectRatio( aspectRatio: controller.videoController.value.aspectRatio, child: VideoPlayer(controller.videoController), ), if (controller.showControls.value) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _iconControl( Icons.replay_10, controller.seekBackward, ), GetBuilder( builder: (vc) => _iconControl( vc.videoController.value.isPlaying ? Icons.pause : Icons.play_arrow, vc.togglePlayPause, size: 48.sp, ), ), _iconControl( Icons.forward_10, controller.seekForward, ), ], ), Positioned( bottom: 8.h, left: 0, right: 0, child: Row( children: [ Expanded( child: VideoProgressIndicator( controller.videoController, allowScrubbing: true, colors: VideoProgressColors( playedColor: Colors.white, bufferedColor: Colors.white30, backgroundColor: Colors.white10, ), ), ), _iconControl(Icons.fullscreen, () { Navigator.push( context, MaterialPageRoute( builder: (_) => FullScreenVideoPlayer(), ), ); }), ], ), ), Positioned( bottom: 36.h, left: 5.w, child: GetBuilder( builder: (vc) { final position = vc.videoController.value.position; final duration = vc.videoController.value.duration; return Text( '${vc.formatDuration(position)} / ${vc.formatDuration(duration)}', style: TextStyle( color: Colors.white70, fontSize: 12.sp, ), ); }, ), ), ], ], ), ); }), ), ], ), ), ); } Widget _buildHeader() { return Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( widget.title, style: TextStyle( color: Colors.white, fontSize: 16.sp, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ), IconButton( onPressed: () => Navigator.pop(context), icon: Icon(Icons.close, color: Colors.white), ), ], ), ); } Widget _iconControl(IconData icon, VoidCallback onTap, {double? size}) { return InkWell( onTap: onTap, child: Container( margin: EdgeInsets.all(8.w), padding: EdgeInsets.all(8.w), decoration: BoxDecoration( color: Colors.black54, shape: BoxShape.circle, ), child: Icon(icon, color: Colors.white, size: size ?? 24.sp), ), ); } } class FullScreenVideoPlayer extends StatefulWidget { const FullScreenVideoPlayer({super.key}); @override State createState() => _FullScreenVideoPlayerState(); } class _FullScreenVideoPlayerState extends State { final controller = Get.find(); @override void initState() { super.initState(); SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); } @override void dispose() { SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: SizedBox.expand( child: GestureDetector( onTap: controller.toggleControls, child: Obx(() { return Stack( fit: StackFit.expand, children: [ Center( child: AspectRatio( aspectRatio: controller.videoController.value.aspectRatio, child: VideoPlayer(controller.videoController), ), ), if (controller.showControls.value) ...[ Positioned( top: 20, right: 20, child: _iconControl( Icons.close, () => Navigator.pop(context), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _iconControl(Icons.replay_10, controller.seekBackward), GetBuilder( builder: (vc) => _iconControl( vc.videoController.value.isPlaying ? Icons.pause : Icons.play_arrow, vc.togglePlayPause, size: 48.sp, ), ), _iconControl(Icons.forward_10, controller.seekForward), ], ), Positioned( bottom: 16.h, left: 16.w, right: 16.w, child: VideoProgressIndicator( controller.videoController, allowScrubbing: true, colors: VideoProgressColors( playedColor: Colors.white, bufferedColor: Colors.white30, backgroundColor: Colors.white10, ), ), ), ], Positioned( bottom: 36.h, left: 20.w, child: GetBuilder( builder: (vc) { final position = vc.videoController.value.position; final duration = vc.videoController.value.duration; return Text( '${vc.formatDuration(position)} / ${vc.formatDuration(duration)}', style: TextStyle( color: Colors.white70, fontSize: 12.sp, ), ); }, ), ), ], ); }), ), ), ); } Widget _iconControl(IconData icon, VoidCallback onTap, {double? size}) { return InkWell( onTap: onTap, child: Container( margin: EdgeInsets.all(8.w), padding: EdgeInsets.all(8.w), decoration: BoxDecoration( color: Colors.black54, shape: BoxShape.circle, ), child: Icon(icon, color: Colors.white, size: size ?? 24.sp), ), ); } }