reproducor en batch añadido, carga automatica

This commit is contained in:
Abraham
2026-01-18 22:12:32 -08:00
parent d3ca3e62e3
commit 29f119038b

View File

@@ -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<PremiumBatchUploadDialog> {
if (input.files == null || input.files!.isEmpty) return;
List<String> 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<PremiumBatchUploadDialog> {
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<PremiumBatchUploadDialog> {
),
),
),
// 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<PremiumBatchUploadDialog> {
);
}
/// 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<PremiumBatchUploadDialog> {
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<PremiumBatchUploadDialog> {
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<PremiumBatchUploadDialog> {
}
}
}
/// 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<void> _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),
),
),
),
),
],
),
),
);
}
}