reproducor en batch añadido, carga automatica
This commit is contained in:
@@ -50,6 +50,9 @@ enum BatchUploadStatus {
|
|||||||
error,
|
error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Límite máximo de tamaño de archivo (10MB)
|
||||||
|
const int _maxFileSizeBytes = 10 * 1024 * 1024; // 10 MB
|
||||||
|
|
||||||
class PremiumBatchUploadDialog extends StatefulWidget {
|
class PremiumBatchUploadDialog extends StatefulWidget {
|
||||||
final VideosProvider provider;
|
final VideosProvider provider;
|
||||||
final VoidCallback onSuccess;
|
final VoidCallback onSuccess;
|
||||||
@@ -97,7 +100,16 @@ class _PremiumBatchUploadDialogState extends State<PremiumBatchUploadDialog> {
|
|||||||
|
|
||||||
if (input.files == null || input.files!.isEmpty) return;
|
if (input.files == null || input.files!.isEmpty) return;
|
||||||
|
|
||||||
|
List<String> rejectedFiles = [];
|
||||||
|
|
||||||
for (var file in input.files!) {
|
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();
|
final reader = html.FileReader();
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
await reader.onLoad.first;
|
await reader.onLoad.first;
|
||||||
@@ -134,6 +146,43 @@ class _PremiumBatchUploadDialogState extends State<PremiumBatchUploadDialog> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_videoQueue.add(videoItem);
|
_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
|
// Botón para cambiar poster
|
||||||
if (!_isUploading && !isMobile)
|
if (!_isUploading && !isMobile)
|
||||||
Positioned(
|
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) {
|
Widget _buildEditFields(BatchVideoItem video, bool isCurrentlyUploading) {
|
||||||
final isEditable = !_isUploading;
|
final isEditable = !_isUploading;
|
||||||
|
|
||||||
@@ -999,18 +1078,18 @@ class _PremiumBatchUploadDialogState extends State<PremiumBatchUploadDialog> {
|
|||||||
Widget _buildActionButtons(BatchVideoItem video) {
|
Widget _buildActionButtons(BatchVideoItem video) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// Generar thumbnail
|
// Reproducir video
|
||||||
_buildSmallActionButton(
|
_buildSmallActionButton(
|
||||||
icon: Icons.auto_fix_high_rounded,
|
icon: Icons.play_circle_rounded,
|
||||||
tooltip: 'Generar miniatura',
|
tooltip: 'Reproducir video',
|
||||||
color: const Color(0xFFFFB733),
|
color: const Color(0xFF00C9A7),
|
||||||
onPressed: () => _generateThumbnail(video),
|
onPressed: () => _showVideoPreview(video),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
// Seleccionar poster
|
// Seleccionar poster
|
||||||
_buildSmallActionButton(
|
_buildSmallActionButton(
|
||||||
icon: Icons.image_rounded,
|
icon: Icons.image_rounded,
|
||||||
tooltip: 'Seleccionar portada',
|
tooltip: 'Cambiar portada',
|
||||||
color: const Color(0xFF4EC9F5),
|
color: const Color(0xFF4EC9F5),
|
||||||
onPressed: () => _selectPosterForVideo(video),
|
onPressed: () => _selectPosterForVideo(video),
|
||||||
),
|
),
|
||||||
@@ -1125,12 +1204,14 @@ class _PremiumBatchUploadDialogState extends State<PremiumBatchUploadDialog> {
|
|||||||
PremiumButton(
|
PremiumButton(
|
||||||
text: _isUploading
|
text: _isUploading
|
||||||
? 'Subiendo...'
|
? 'Subiendo...'
|
||||||
: 'Subir ${_videoQueue.length} video(s)',
|
: _videoQueue.length == 1
|
||||||
|
? 'Subir 1 video'
|
||||||
|
: 'Subir ${_videoQueue.length} videos',
|
||||||
icon: _isUploading ? null : Icons.cloud_upload_rounded,
|
icon: _isUploading ? null : Icons.cloud_upload_rounded,
|
||||||
onPressed:
|
onPressed:
|
||||||
_isUploading || _videoQueue.isEmpty ? null : _startBatchUpload,
|
_isUploading || _videoQueue.isEmpty ? null : _startBatchUpload,
|
||||||
backgroundColor: const Color(0xFF00C9A7),
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user