reproducor en batch añadido, carga automatica
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user