diff --git a/lib/pages/videos/widgets/premium_batch_upload_dialog.dart b/lib/pages/videos/widgets/premium_batch_upload_dialog.dart index 565a6ad..ff9b4d9 100644 --- a/lib/pages/videos/widgets/premium_batch_upload_dialog.dart +++ b/lib/pages/videos/widgets/premium_batch_upload_dialog.dart @@ -50,6 +50,9 @@ enum BatchUploadStatus { error, } +/// Límite máximo de tamaño de archivo (10MB) +const int _maxFileSizeBytes = 10 * 1024 * 1024; // 10 MB + class PremiumBatchUploadDialog extends StatefulWidget { final VideosProvider provider; final VoidCallback onSuccess; @@ -97,7 +100,16 @@ class _PremiumBatchUploadDialogState extends State { if (input.files == null || input.files!.isEmpty) return; + List rejectedFiles = []; + for (var file in input.files!) { + // Validar tamaño del archivo (máximo 10MB) + if (file.size > _maxFileSizeBytes) { + final sizeMB = (file.size / (1024 * 1024)).toStringAsFixed(1); + rejectedFiles.add('${file.name} (${sizeMB}MB)'); + continue; + } + final reader = html.FileReader(); reader.readAsArrayBuffer(file); await reader.onLoad.first; @@ -134,6 +146,43 @@ class _PremiumBatchUploadDialogState extends State { setState(() { _videoQueue.add(videoItem); }); + + // Generar thumbnail automáticamente después de agregar + _generateThumbnail(videoItem); + } + + // Mostrar mensaje si hubo archivos rechazados por tamaño + if (rejectedFiles.isNotEmpty && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.warning_rounded, color: Colors.white, size: 20), + Gap(8), + Text( + 'Videos rechazados (máx. 10MB):', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const Gap(4), + Text( + rejectedFiles.join(', '), + style: const TextStyle(fontSize: 12), + ), + ], + ), + backgroundColor: const Color(0xFFFF7A3D), + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 5), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ); } } @@ -786,6 +835,26 @@ class _PremiumBatchUploadDialogState extends State { ), ), ), + // Botón de play para previsualizar video + Positioned.fill( + child: Center( + child: GestureDetector( + onTap: () => _showVideoPreview(video), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + shape: BoxShape.circle, + ), + child: Icon( + Icons.play_arrow_rounded, + color: Colors.white, + size: isMobile ? 20 : 28, + ), + ), + ), + ), + ), // Botón para cambiar poster if (!_isUploading && !isMobile) Positioned( @@ -811,6 +880,16 @@ class _PremiumBatchUploadDialogState extends State { ); } + /// Mostrar preview del video en un dialog + void _showVideoPreview(BatchVideoItem video) { + if (video.blobUrl == null) return; + + showDialog( + context: context, + builder: (context) => _VideoPreviewDialog(blobUrl: video.blobUrl!), + ); + } + Widget _buildEditFields(BatchVideoItem video, bool isCurrentlyUploading) { final isEditable = !_isUploading; @@ -999,18 +1078,18 @@ class _PremiumBatchUploadDialogState extends State { Widget _buildActionButtons(BatchVideoItem video) { return Column( children: [ - // Generar thumbnail + // Reproducir video _buildSmallActionButton( - icon: Icons.auto_fix_high_rounded, - tooltip: 'Generar miniatura', - color: const Color(0xFFFFB733), - onPressed: () => _generateThumbnail(video), + icon: Icons.play_circle_rounded, + tooltip: 'Reproducir video', + color: const Color(0xFF00C9A7), + onPressed: () => _showVideoPreview(video), ), const Gap(8), // Seleccionar poster _buildSmallActionButton( icon: Icons.image_rounded, - tooltip: 'Seleccionar portada', + tooltip: 'Cambiar portada', color: const Color(0xFF4EC9F5), onPressed: () => _selectPosterForVideo(video), ), @@ -1125,12 +1204,14 @@ class _PremiumBatchUploadDialogState extends State { PremiumButton( text: _isUploading ? 'Subiendo...' - : 'Subir ${_videoQueue.length} video(s)', + : _videoQueue.length == 1 + ? 'Subir 1 video' + : 'Subir ${_videoQueue.length} videos', icon: _isUploading ? null : Icons.cloud_upload_rounded, onPressed: _isUploading || _videoQueue.isEmpty ? null : _startBatchUpload, backgroundColor: const Color(0xFF00C9A7), - width: 200, + width: 220, ), ], ), @@ -1191,3 +1272,264 @@ class _PremiumBatchUploadDialogState extends State { } } } + +/// Dialog para previsualizar el video antes de subir +class _VideoPreviewDialog extends StatefulWidget { + final String blobUrl; + + const _VideoPreviewDialog({required this.blobUrl}); + + @override + State<_VideoPreviewDialog> createState() => _VideoPreviewDialogState(); +} + +class _VideoPreviewDialogState extends State<_VideoPreviewDialog> { + VideoPlayerController? _controller; + bool _isInitialized = false; + bool _isPlaying = false; + + @override + void initState() { + super.initState(); + _initializeVideo(); + } + + Future _initializeVideo() async { + _controller = VideoPlayerController.network(widget.blobUrl); + try { + await _controller!.initialize(); + setState(() => _isInitialized = true); + _controller!.addListener(() { + if (mounted) { + setState(() => _isPlaying = _controller!.value.isPlaying); + } + }); + } catch (e) { + debugPrint('Error inicializando video preview: $e'); + } + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + String _formatPosition(Duration position) { + final minutes = position.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = position.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.all(24), + child: Container( + constraints: BoxConstraints( + maxWidth: 800, + maxHeight: MediaQuery.of(context).size.height * 0.8, + ), + decoration: BoxDecoration( + color: const Color(0xFF121214), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFFFFB733)], + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + const Icon(Icons.play_circle_rounded, + color: Colors.white, size: 24), + const Gap(12), + const Expanded( + child: Text( + 'Vista Previa del Video', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + ), + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + ), + ), + // Video Player + Flexible( + child: _isInitialized + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: AspectRatio( + aspectRatio: _controller!.value.aspectRatio, + child: VideoPlayer(_controller!), + ), + ), + // Controls + Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // Progress bar + ValueListenableBuilder( + valueListenable: _controller!, + builder: + (context, VideoPlayerValue value, child) { + return Column( + children: [ + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: + const Color(0xFF4EC9F5), + inactiveTrackColor: + Colors.grey.shade800, + thumbColor: const Color(0xFFFFB733), + trackHeight: 4, + ), + child: Slider( + value: value.position.inMilliseconds + .toDouble(), + max: value.duration.inMilliseconds + .toDouble(), + onChanged: (newValue) { + _controller!.seekTo( + Duration( + milliseconds: + newValue.toInt()), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatPosition(value.position), + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + fontFamily: 'Poppins', + ), + ), + Text( + _formatPosition(value.duration), + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ); + }, + ), + const Gap(8), + // Play/Pause button + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + _controller!.seekTo( + _controller!.value.position - + const Duration(seconds: 10), + ); + }, + icon: const Icon( + Icons.replay_10_rounded, + color: Colors.white70, + size: 32, + ), + ), + const Gap(16), + GestureDetector( + onTap: () { + if (_isPlaying) { + _controller!.pause(); + } else { + _controller!.play(); + } + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFF4EC9F5), + Color(0xFFFFB733) + ], + ), + shape: BoxShape.circle, + ), + child: Icon( + _isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + color: Colors.white, + size: 36, + ), + ), + ), + const Gap(16), + IconButton( + onPressed: () { + _controller!.seekTo( + _controller!.value.position + + const Duration(seconds: 10), + ); + }, + icon: const Icon( + Icons.forward_10_rounded, + color: Colors.white70, + size: 32, + ), + ), + ], + ), + ], + ), + ), + ], + ) + : const Center( + child: Padding( + padding: EdgeInsets.all(40), + child: CircularProgressIndicator( + color: Color(0xFF4EC9F5), + ), + ), + ), + ), + ], + ), + ), + ); + } +}