698 lines
21 KiB
Dart
698 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:onufitness/screens/u_vault/controllers/uvault_video_controller.dart';
|
|
import 'package:onufitness/utils/helper_function.dart';
|
|
import 'package:shimmer/shimmer.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
import 'package:chewie/chewie.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:onufitness/constants/color_constant.dart';
|
|
|
|
class VideoCard extends StatefulWidget {
|
|
final String? title;
|
|
final String category;
|
|
final String videoUrl;
|
|
final String? videoId;
|
|
final String? thumbnailUrl;
|
|
final bool isMyVideoScreen;
|
|
final VoidCallback? onPressed;
|
|
const VideoCard({
|
|
super.key,
|
|
this.title,
|
|
required this.category,
|
|
required this.videoUrl,
|
|
this.videoId,
|
|
this.thumbnailUrl,
|
|
required this.isMyVideoScreen,
|
|
this.onPressed,
|
|
});
|
|
|
|
@override
|
|
State<VideoCard> createState() => _VideoCardState();
|
|
}
|
|
|
|
class _VideoCardState extends State<VideoCard> {
|
|
final controller = Get.find<UvaultController>();
|
|
ChewieController? _chewieController;
|
|
VideoPlayerController? _videoPlayerController;
|
|
bool _isVideoInitialized = false;
|
|
bool _isVideoPlaying = false;
|
|
bool _isInitializing = false;
|
|
bool _isDisposed = false;
|
|
|
|
Future<void> _initializeVideo() async {
|
|
if (_isInitializing || _isVideoInitialized || _isDisposed) return;
|
|
|
|
setState(() {
|
|
_isInitializing = true;
|
|
});
|
|
|
|
try {
|
|
controller.setCurrentPlayingVideo(widget.videoUrl);
|
|
_videoPlayerController = VideoPlayerController.networkUrl(
|
|
Uri.parse(widget.videoUrl),
|
|
);
|
|
|
|
await _videoPlayerController!.initialize();
|
|
if (_isDisposed || !mounted) {
|
|
await _videoPlayerController!.dispose();
|
|
return;
|
|
}
|
|
|
|
await _videoPlayerController!.setVolume(1.0);
|
|
|
|
_chewieController = ChewieController(
|
|
// Add Android-specific Color customization
|
|
materialProgressColors: ChewieProgressColors(
|
|
playedColor: Color(primaryColor),
|
|
handleColor: Color(primaryColor),
|
|
backgroundColor: Colors.grey,
|
|
bufferedColor: Colors.grey.shade300,
|
|
),
|
|
// Add iOS-specific Color customization
|
|
cupertinoProgressColors: ChewieProgressColors(
|
|
playedColor: Color(primaryColor),
|
|
handleColor: Color(primaryColor),
|
|
backgroundColor: Colors.grey,
|
|
bufferedColor: Colors.grey.shade300,
|
|
),
|
|
videoPlayerController: _videoPlayerController!,
|
|
autoPlay: true,
|
|
looping: false,
|
|
showControls: true,
|
|
aspectRatio: _videoPlayerController!.value.aspectRatio,
|
|
deviceOrientationsAfterFullScreen: [DeviceOrientation.portraitUp],
|
|
errorBuilder: (context, errorMessage) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error, color: Colors.red, size: 40.sp),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
'Video playback error',
|
|
style: TextStyle(color: Colors.red, fontSize: 12.sp),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
if (mounted && !_isDisposed) {
|
|
setState(() {
|
|
_isVideoInitialized = true;
|
|
_isVideoPlaying = true;
|
|
_isInitializing = false;
|
|
});
|
|
} else {
|
|
// Clean up if component was disposed during initialization
|
|
await _disposeVideoControllers();
|
|
}
|
|
} catch (e) {
|
|
if (mounted && !_isDisposed) {
|
|
setState(() {
|
|
_isInitializing = false;
|
|
});
|
|
}
|
|
await _disposeVideoControllers();
|
|
}
|
|
}
|
|
|
|
Future<void> _disposeVideoControllers() async {
|
|
if (_chewieController != null) {
|
|
_chewieController!.dispose();
|
|
_chewieController!.pause();
|
|
_chewieController = null;
|
|
}
|
|
|
|
if (_videoPlayerController != null) {
|
|
await _videoPlayerController!.dispose();
|
|
await _videoPlayerController!.pause();
|
|
_videoPlayerController = null;
|
|
}
|
|
|
|
if (mounted && !_isDisposed) {
|
|
setState(() {
|
|
_isVideoInitialized = false;
|
|
_isVideoPlaying = false;
|
|
_isInitializing = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _onThumbnailTap() {
|
|
if (!_isInitializing && !_isVideoInitialized) {
|
|
_initializeVideo();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_isDisposed = true;
|
|
_disposeVideoControllers();
|
|
super.dispose();
|
|
}
|
|
|
|
Widget _buildThumbnailView() {
|
|
return Stack(
|
|
children: [
|
|
// Thumbnail Image
|
|
if (widget.thumbnailUrl != null && widget.thumbnailUrl!.isNotEmpty)
|
|
Image.network(
|
|
widget.thumbnailUrl ??
|
|
"https://placehold.co/400?text=Thumbnail&font=roboto",
|
|
height: isTablet ? 250.h : 200.h,
|
|
width: double.infinity,
|
|
fit: BoxFit.cover,
|
|
loadingBuilder: (context, child, loadingProgress) {
|
|
if (loadingProgress == null) return child;
|
|
return _buildShimmerPlaceholder();
|
|
},
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return _buildErrorPlaceholder();
|
|
},
|
|
)
|
|
else
|
|
_buildErrorPlaceholder(),
|
|
|
|
// Play Button Overlay
|
|
Positioned.fill(
|
|
child: Container(
|
|
color: Colors.black26,
|
|
child: Center(
|
|
child: GestureDetector(
|
|
onTap: _onThumbnailTap,
|
|
child: Container(
|
|
width: 60.w,
|
|
height: 60.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.black54,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.play_arrow,
|
|
color: Colors.white,
|
|
size: 30.sp,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Loading indicator when initializing
|
|
if (_isInitializing)
|
|
Positioned.fill(
|
|
child: Container(
|
|
color: Colors.black26,
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildVideoView() {
|
|
if (_chewieController == null) {
|
|
return Container(
|
|
height: isTablet ? 250.h : 200,
|
|
width: double.infinity,
|
|
color: Colors.black,
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Calculate proper height based on aspect ratio
|
|
final aspectRatio = _videoPlayerController?.value.aspectRatio ?? 16 / 9;
|
|
final videoHeight = MediaQuery.of(context).size.width / aspectRatio;
|
|
|
|
// Set minimum and maximum heights to prevent extreme sizes
|
|
final minHeight = 200.h;
|
|
final maxHeight = 300.h;
|
|
final finalHeight = videoHeight.clamp(minHeight, maxHeight);
|
|
|
|
return SizedBox(
|
|
height: finalHeight,
|
|
width: double.infinity,
|
|
child: AspectRatio(
|
|
aspectRatio: aspectRatio,
|
|
child: Chewie(controller: _chewieController!),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildShimmerPlaceholder() {
|
|
return Shimmer.fromColors(
|
|
baseColor: Colors.grey.shade300,
|
|
highlightColor: Colors.grey.shade100,
|
|
child: Container(
|
|
height: isTablet ? 250.h : 200,
|
|
width: double.infinity,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildErrorPlaceholder() {
|
|
return Container(
|
|
height: isTablet ? 250.h : 200,
|
|
width: double.infinity,
|
|
color: Colors.grey.shade200,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.video_library_outlined,
|
|
size: 40.sp,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
SizedBox(height: 8.h),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: BoxDecoration(color: Colors.white),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ClipRRect(
|
|
// borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
|
|
child:
|
|
_isVideoInitialized &&
|
|
_isVideoPlaying &&
|
|
_chewieController != null &&
|
|
!_isDisposed
|
|
? _buildVideoView()
|
|
: _buildThumbnailView(),
|
|
),
|
|
widget.title != null
|
|
? Padding(
|
|
padding: EdgeInsets.all(8.0.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
widget.category,
|
|
style: TextStyle(fontSize: 14.sp, color: greyTextColor1),
|
|
),
|
|
SizedBox(height: 4.h),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
widget.title!,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
color: Colors.black,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
if (widget.isMyVideoScreen)
|
|
IconButton(
|
|
onPressed: widget.onPressed,
|
|
icon: Icon(Icons.more_vert),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: Container(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
//.........Above one is Chewie Player......................................................................................................
|
|
//.........Below one is Better Player......................................................................................................
|
|
|
|
// import 'dart:developer';
|
|
// import 'package:flutter/material.dart';
|
|
// import 'package:flutter/services.dart';
|
|
// import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
// import 'package:onufitness/screens/u_vault/controllers/uvault_video_controller.dart';
|
|
// import 'package:shimmer/shimmer.dart';
|
|
// import 'package:better_player_plus/better_player_plus.dart';
|
|
// import 'package:get/get.dart';
|
|
// import 'package:onufitness/constants/color_constant.dart';
|
|
|
|
// class VideoCard extends StatefulWidget {
|
|
// final String title;
|
|
// final String category;
|
|
// final String videoUrl;
|
|
// final String? videoId;
|
|
// final String? thumbnailUrl;
|
|
// final bool isMyVideoScreen;
|
|
// final VoidCallback? onPressed;
|
|
|
|
// const VideoCard({
|
|
// super.key,
|
|
// required this.title,
|
|
// required this.category,
|
|
// required this.videoUrl,
|
|
// this.videoId,
|
|
// this.thumbnailUrl,
|
|
// required this.isMyVideoScreen,
|
|
// this.onPressed,
|
|
// });
|
|
|
|
// @override
|
|
// State<VideoCard> createState() => _VideoCardState();
|
|
// }
|
|
|
|
// class _VideoCardState extends State<VideoCard> {
|
|
// final controller = Get.find<UvaultController>();
|
|
// BetterPlayerController? _betterPlayerController;
|
|
// bool _isVideoInitialized = false;
|
|
// bool _isVideoPlaying = false;
|
|
// bool _isInitializing = false;
|
|
// bool _isDisposed = false;
|
|
|
|
// Future<void> _initializeVideo() async {
|
|
// if (_isInitializing || _isVideoInitialized || _isDisposed) return;
|
|
|
|
// setState(() {
|
|
// _isInitializing = true;
|
|
// });
|
|
|
|
// try {
|
|
// controller.setCurrentPlayingVideo(widget.videoUrl);
|
|
|
|
// // Create BetterPlayer configuration
|
|
// BetterPlayerConfiguration betterPlayerConfiguration =
|
|
// BetterPlayerConfiguration(
|
|
// aspectRatio: 16 / 9,
|
|
// fit: BoxFit.contain,
|
|
// autoPlay: true,
|
|
// looping: false,
|
|
// fullScreenByDefault: false,
|
|
// allowedScreenSleep: false,
|
|
// deviceOrientationsAfterFullScreen: [DeviceOrientation.portraitUp],
|
|
// controlsConfiguration: BetterPlayerControlsConfiguration(
|
|
// showControls: true,
|
|
// enableOverflowMenu: false,
|
|
// enableFullscreen: true,
|
|
// enablePip: false,
|
|
// enablePlayPause: true,
|
|
// enableMute: true,
|
|
// enableProgressBar: true,
|
|
// controlBarColor: Colors.black.withValues(alpha: 0.5),
|
|
// progressBarPlayedColor: Color(primaryColor),
|
|
// progressBarHandleColor: Color(primaryColor),
|
|
// progressBarBufferedColor: Colors.grey.shade300,
|
|
// progressBarBackgroundColor: Colors.grey,
|
|
// iconsColor: Colors.white,
|
|
// ),
|
|
// errorBuilder: (context, errorMessage) {
|
|
// return Center(
|
|
// child: Column(
|
|
// mainAxisAlignment: MainAxisAlignment.center,
|
|
// children: [
|
|
// Icon(Icons.error, color: Colors.red, size: 40.sp),
|
|
// SizedBox(height: 8.h),
|
|
// Text(
|
|
// 'Video playback error',
|
|
// style: TextStyle(color: Colors.red, fontSize: 12.sp),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// );
|
|
// },
|
|
// );
|
|
|
|
// // Create data source
|
|
// BetterPlayerDataSource dataSource = BetterPlayerDataSource(
|
|
// BetterPlayerDataSourceType.network,
|
|
// widget.videoUrl,
|
|
// videoFormat: BetterPlayerVideoFormat.other,
|
|
// );
|
|
|
|
// // Initialize BetterPlayer
|
|
// _betterPlayerController = BetterPlayerController(
|
|
// betterPlayerConfiguration,
|
|
// );
|
|
// await _betterPlayerController!.setupDataSource(dataSource);
|
|
|
|
// if (_isDisposed || !mounted) {
|
|
// // If disposed during initialization, clean up
|
|
// _betterPlayerController?.dispose();
|
|
// return;
|
|
// }
|
|
|
|
// // Set volume
|
|
// await _betterPlayerController!.setVolume(1.0);
|
|
|
|
// if (mounted && !_isDisposed) {
|
|
// setState(() {
|
|
// _isVideoInitialized = true;
|
|
// _isVideoPlaying = true;
|
|
// _isInitializing = false;
|
|
// });
|
|
// } else {
|
|
// // Clean up if component was disposed during initialization
|
|
// _disposeVideoController();
|
|
// }
|
|
// } catch (e) {
|
|
// log('Error initializing video: $e');
|
|
// if (mounted && !_isDisposed) {
|
|
// setState(() {
|
|
// _isInitializing = false;
|
|
// });
|
|
// }
|
|
// _disposeVideoController();
|
|
// }
|
|
// }
|
|
|
|
// void _disposeVideoController() {
|
|
// if (_betterPlayerController != null) {
|
|
// _betterPlayerController!.dispose();
|
|
// _betterPlayerController = null;
|
|
// }
|
|
|
|
// if (mounted && !_isDisposed) {
|
|
// setState(() {
|
|
// _isVideoInitialized = false;
|
|
// _isVideoPlaying = false;
|
|
// _isInitializing = false;
|
|
// });
|
|
// }
|
|
// }
|
|
|
|
// void _onThumbnailTap() {
|
|
// if (!_isInitializing && !_isVideoInitialized) {
|
|
// _initializeVideo();
|
|
// }
|
|
// log(widget.videoUrl);
|
|
// }
|
|
|
|
// @override
|
|
// void dispose() {
|
|
// _isDisposed = true;
|
|
// // Clean up video controller
|
|
// _disposeVideoController();
|
|
// super.dispose();
|
|
// }
|
|
|
|
// Widget _buildThumbnailView() {
|
|
// return Stack(
|
|
// children: [
|
|
// // Thumbnail Image
|
|
// if (widget.thumbnailUrl != null && widget.thumbnailUrl!.isNotEmpty)
|
|
// Image.network(
|
|
// widget.thumbnailUrl ??
|
|
// "https://placehold.co/400?text=Thumbnail&font=roboto",
|
|
// height: 180.h,
|
|
// width: double.infinity,
|
|
// fit: BoxFit.cover,
|
|
// loadingBuilder: (context, child, loadingProgress) {
|
|
// if (loadingProgress == null) return child;
|
|
// return _buildShimmerPlaceholder();
|
|
// },
|
|
// errorBuilder: (context, error, stackTrace) {
|
|
// return _buildErrorPlaceholder();
|
|
// },
|
|
// )
|
|
// else
|
|
// _buildErrorPlaceholder(),
|
|
|
|
// // Play Button Overlay
|
|
// Positioned.fill(
|
|
// child: Container(
|
|
// color: Colors.black26,
|
|
// child: Center(
|
|
// child: GestureDetector(
|
|
// onTap: _onThumbnailTap,
|
|
// child: Container(
|
|
// width: 60.w,
|
|
// height: 60.h,
|
|
// decoration: BoxDecoration(
|
|
// color: Colors.black54,
|
|
// shape: BoxShape.circle,
|
|
// ),
|
|
// child: Icon(
|
|
// Icons.play_arrow,
|
|
// color: Colors.white,
|
|
// size: 30.sp,
|
|
// ),
|
|
// ),
|
|
// ),
|
|
// ),
|
|
// ),
|
|
// ),
|
|
|
|
// // Loading indicator when initializing
|
|
// if (_isInitializing)
|
|
// Positioned.fill(
|
|
// child: Container(
|
|
// color: Colors.black26,
|
|
// child: Center(
|
|
// child: CircularProgressIndicator(
|
|
// valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
// ),
|
|
// ),
|
|
// ),
|
|
// ),
|
|
// ],
|
|
// );
|
|
// }
|
|
|
|
// Widget _buildVideoView() {
|
|
// if (_betterPlayerController == null) {
|
|
// return Container(
|
|
// height: 180.h,
|
|
// width: double.infinity,
|
|
// color: Colors.black,
|
|
// child: Center(
|
|
// child: CircularProgressIndicator(
|
|
// valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
// ),
|
|
// ),
|
|
// );
|
|
// }
|
|
|
|
// return SizedBox(
|
|
// height: 180.h,
|
|
// width: double.infinity,
|
|
// child: BetterPlayer(controller: _betterPlayerController!),
|
|
// );
|
|
// }
|
|
|
|
// Widget _buildShimmerPlaceholder() {
|
|
// return Shimmer.fromColors(
|
|
// baseColor: Colors.grey.shade300,
|
|
// highlightColor: Colors.grey.shade100,
|
|
// child: Container(
|
|
// height: 180.h,
|
|
// width: double.infinity,
|
|
// color: Colors.grey.shade300,
|
|
// ),
|
|
// );
|
|
// }
|
|
|
|
// Widget _buildErrorPlaceholder() {
|
|
// return Container(
|
|
// height: 180.h,
|
|
// width: double.infinity,
|
|
// color: Colors.grey.shade200,
|
|
// child: Column(
|
|
// mainAxisAlignment: MainAxisAlignment.center,
|
|
// children: [
|
|
// Icon(
|
|
// Icons.video_library_outlined,
|
|
// size: 40.sp,
|
|
// color: Colors.grey.shade500,
|
|
// ),
|
|
// SizedBox(height: 8.h),
|
|
// Text(
|
|
// 'Video Thumbnail',
|
|
// style: TextStyle(color: Colors.grey.shade500, fontSize: 12.sp),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// );
|
|
// }
|
|
|
|
// @override
|
|
// Widget build(BuildContext context) {
|
|
// log("rebuild video card");
|
|
// return Container(
|
|
// decoration: BoxDecoration(
|
|
// color: Colors.white,
|
|
// borderRadius: BorderRadius.circular(12.r),
|
|
// boxShadow: [
|
|
// BoxShadow(color: Colors.black12, blurRadius: 4, spreadRadius: 2),
|
|
// ],
|
|
// ),
|
|
// child: Column(
|
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
// children: [
|
|
// ClipRRect(
|
|
// borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
|
|
// child:
|
|
// _isVideoInitialized &&
|
|
// _isVideoPlaying &&
|
|
// _betterPlayerController != null &&
|
|
// !_isDisposed
|
|
// ? _buildVideoView()
|
|
// : _buildThumbnailView(),
|
|
// ),
|
|
// Padding(
|
|
// padding: EdgeInsets.all(8.0.w),
|
|
// child: Column(
|
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
// children: [
|
|
// Text(
|
|
// widget.category,
|
|
// style: TextStyle(fontSize: 14.sp, color: greyTextColor1),
|
|
// ),
|
|
// SizedBox(height: 4.h),
|
|
// Row(
|
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
// children: [
|
|
// Expanded(
|
|
// child: Text(
|
|
// widget.title,
|
|
// maxLines: 2,
|
|
// overflow: TextOverflow.ellipsis,
|
|
// style: TextStyle(
|
|
// fontSize: 15.sp,
|
|
// color: Colors.black,
|
|
// fontWeight: FontWeight.w600,
|
|
// ),
|
|
// ),
|
|
// ),
|
|
// if (widget.isMyVideoScreen)
|
|
// IconButton(
|
|
// onPressed: widget.onPressed,
|
|
// icon: Icon(Icons.more_horiz),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// );
|
|
// }
|
|
// }
|