From 974282ef1ee00dc8e9f2e10d3d8e8cf122d95876 Mon Sep 17 00:00:00 2001 From: Abraham Date: Thu, 15 Jan 2026 22:50:14 -0800 Subject: [PATCH] boton temporal para actualizar duraciones, fix de duracion en y peso de el archvio --- lib/pages/videos/gestor_videos_page.dart | 98 +++++++- .../edit_video_dialog.dart | 39 ++++ .../update_durations_button.dart | 217 ++++++++++++++++++ .../videos/widgets/premium_upload_dialog.dart | 7 + lib/providers/videos_provider.dart | 93 ++++++++ lib/theme/theme.dart | 5 + 6 files changed, 456 insertions(+), 3 deletions(-) create mode 100644 lib/pages/videos/widgets/gestor_videos_widgets/update_durations_button.dart diff --git a/lib/pages/videos/gestor_videos_page.dart b/lib/pages/videos/gestor_videos_page.dart index 4520adc..bdbed8d 100644 --- a/lib/pages/videos/gestor_videos_page.dart +++ b/lib/pages/videos/gestor_videos_page.dart @@ -11,6 +11,7 @@ import 'package:energy_media/pages/videos/widgets/video_player_dialog.dart'; import 'package:energy_media/pages/videos/widgets/gestor_videos_widgets/empty_state_widget.dart'; import 'package:energy_media/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart'; import 'package:energy_media/pages/videos/widgets/gestor_videos_widgets/delete_video_dialog.dart'; + import 'package:energy_media/pages/videos/widgets/video_thumbnail_widget.dart'; import 'package:gap/gap.dart'; @@ -437,7 +438,7 @@ class _GestorVideosPageState extends State { title: 'Descripción', field: 'file_description', type: PlutoColumnType.text(), - width: 425, + width: 400, enableEditingMode: false, renderer: (rendererContext) { final description = rendererContext.cell.value?.toString() ?? ''; @@ -523,14 +524,105 @@ class _GestorVideosPageState extends State { title: 'Duración', field: 'duration', type: PlutoColumnType.text(), - width: 180, + width: 120, enableEditingMode: false, + textAlign: PlutoColumnTextAlign.center, + renderer: (rendererContext) { + final duration = rendererContext.cell.value?.toString() ?? '-'; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppTheme.of(context).warning.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).warning.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.access_time_rounded, + size: 14, + color: AppTheme.of(context).warning, + ), + const Gap(6), + Flexible( + child: Text( + duration, + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppTheme.of(context).warning, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + }, + ), + PlutoColumn( + title: 'Tamaño', + field: 'file_size', + type: PlutoColumnType.text(), + width: 120, + enableEditingMode: false, + textAlign: PlutoColumnTextAlign.center, + renderer: (rendererContext) { + final fileSize = rendererContext.cell.value?.toString() ?? '-'; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppTheme.of(context).info.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).info.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.storage_rounded, + size: 14, + color: AppTheme.of(context).info, + ), + const Gap(6), + Flexible( + child: Text( + fileSize, + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppTheme.of(context).info, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + }, ), PlutoColumn( title: 'Fecha de Creación', field: 'createdAt', type: PlutoColumnType.text(), - width: 280, + width: 240, enableEditingMode: false, renderer: (rendererContext) { final video = diff --git a/lib/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart b/lib/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart index 3e223a4..1bfe17c 100644 --- a/lib/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart +++ b/lib/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart @@ -9,6 +9,7 @@ import 'package:gap/gap.dart'; import 'package:energy_media/theme/theme.dart'; import 'package:energy_media/models/media/media_models.dart'; import 'package:energy_media/providers/videos_provider.dart'; +import 'package:energy_media/helpers/globals.dart'; class EditVideoDialog extends StatefulWidget { final MediaFileModel video; @@ -93,6 +94,17 @@ class _EditVideoDialogState extends State { ), ); + // Capturar duración automáticamente si no existe + if (widget.video.seconds == null) { + final durationSeconds = + _videoPlayerController!.value.duration.inSeconds; + if (durationSeconds > 0) { + await _saveDurationToDatabase(durationSeconds); + debugPrint( + '✅ Duración capturada automáticamente: $durationSeconds segundos'); + } + } + setState(() => _isVideoLoading = false); } catch (e) { setState(() => _isVideoLoading = false); @@ -160,6 +172,33 @@ class _EditVideoDialogState extends State { } } + /// Guardar duración capturada en la base de datos + Future _saveDurationToDatabase(int durationSeconds) async { + try { + // Actualizar tanto seconds como metadata_json + final response = await supabaseML + .from('media_files') + .select('metadata_json') + .eq('media_file_id', widget.video.mediaFileId) + .eq('organization_fk', VideosProvider.organizationId) + .single(); + + final metadata = response['metadata_json'] as Map? ?? {}; + metadata['duration_seconds'] = durationSeconds; + + await supabaseML + .from('media_files') + .update({ + 'seconds': durationSeconds, + 'metadata_json': metadata, + }) + .eq('media_file_id', widget.video.mediaFileId) + .eq('organization_fk', VideosProvider.organizationId); + } catch (e) { + debugPrint('Error guardando duración: $e'); + } + } + Future _saveChanges() async { // Actualizar título if (titleController.text != widget.video.title) { diff --git a/lib/pages/videos/widgets/gestor_videos_widgets/update_durations_button.dart b/lib/pages/videos/widgets/gestor_videos_widgets/update_durations_button.dart new file mode 100644 index 0000000..efe9975 --- /dev/null +++ b/lib/pages/videos/widgets/gestor_videos_widgets/update_durations_button.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:gap/gap.dart'; +import 'package:energy_media/providers/videos_provider.dart'; +import 'package:energy_media/theme/theme.dart'; +import 'package:energy_media/widgets/premium_button.dart'; + +/// Botón condicional para actualizar duraciones de videos +/// Se muestra solo cuando hay videos sin duración +/// Una vez procesados todos, desaparece automáticamente +class UpdateDurationsButton extends StatelessWidget { + const UpdateDurationsButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, child) { + final videosWithoutDuration = + provider.mediaFiles.where((video) => video.seconds == null).length; + + // Si no hay videos sin duración, no mostrar nada + if (videosWithoutDuration == 0) { + return const SizedBox.shrink(); + } + + // Mostrar botón con contador + return PremiumButton( + text: 'Actualizar duraciones ($videosWithoutDuration)', + icon: Icons.update_rounded, + onPressed: () => _showUpdateDialog(context, provider), + ); + }, + ); + } + + Future _showUpdateDialog( + BuildContext context, VideosProvider provider) async { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => _UpdateDurationsDialog(provider: provider), + ); + } +} + +class _UpdateDurationsDialog extends StatefulWidget { + final VideosProvider provider; + + const _UpdateDurationsDialog({required this.provider}); + + @override + State<_UpdateDurationsDialog> createState() => _UpdateDurationsDialogState(); +} + +class _UpdateDurationsDialogState extends State<_UpdateDurationsDialog> { + int current = 0; + int total = 0; + bool isProcessing = true; + String? errorMessage; + + @override + void initState() { + super.initState(); + _startProcessing(); + } + + Future _startProcessing() async { + final result = await widget.provider.updateMissingDurations((curr, tot) { + if (mounted) { + setState(() { + current = curr; + total = tot; + }); + } + }); + + if (mounted) { + if (result['success'] == true) { + setState(() { + isProcessing = false; + }); + + // Esperar un momento para mostrar el resultado + await Future.delayed(const Duration(seconds: 1)); + + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '✅ ${result['updated']} videos actualizados correctamente', + ), + backgroundColor: Colors.green, + ), + ); + } + } else { + setState(() { + isProcessing = false; + errorMessage = result['error'] ?? 'Error desconocido'; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: 400, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + blurRadius: 40, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).primaryColor.withOpacity(0.2), + AppTheme.of(context).secondaryColor.withOpacity(0.2), + ], + ), + shape: BoxShape.circle, + ), + child: Icon( + isProcessing ? Icons.update_rounded : Icons.check_circle, + size: 48, + color: isProcessing + ? AppTheme.of(context).primaryColor + : Colors.green, + ), + ), + const Gap(24), + Text( + isProcessing + ? 'Actualizando duraciones' + : errorMessage != null + ? 'Error' + : '¡Completado!', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const Gap(16), + if (isProcessing) ...[ + Text( + 'Procesando $current de $total videos...', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryText, + ), + ), + const Gap(24), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: LinearProgressIndicator( + value: total > 0 ? current / total : 0, + minHeight: 8, + backgroundColor: AppTheme.of(context).tertiaryBackground, + valueColor: AlwaysStoppedAnimation( + AppTheme.of(context).primaryColor, + ), + ), + ), + ] else if (errorMessage != null) ...[ + Text( + errorMessage!, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).error, + ), + textAlign: TextAlign.center, + ), + const Gap(24), + ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.of(context).error, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + child: const Text('Cerrar'), + ), + ] else ...[ + Text( + 'Todas las duraciones han sido actualizadas', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryText, + ), + textAlign: TextAlign.center, + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/pages/videos/widgets/premium_upload_dialog.dart b/lib/pages/videos/widgets/premium_upload_dialog.dart index 0bb0a71..bde0867 100644 --- a/lib/pages/videos/widgets/premium_upload_dialog.dart +++ b/lib/pages/videos/widgets/premium_upload_dialog.dart @@ -38,6 +38,7 @@ class _PremiumUploadDialogState extends State { bool isUploading = false; bool _isVideoLoading = false; String? _videoBlobUrl; + int? _videoDurationSeconds; // Duración capturada del video @override void dispose() { @@ -108,6 +109,11 @@ class _PremiumUploadDialogState extends State { ), ); + // Capturar duración del video + _videoDurationSeconds = _videoController!.value.duration.inSeconds; + debugPrint( + '🕒 Duración del video capturada: $_videoDurationSeconds segundos'); + setState(() => _isVideoLoading = false); } catch (e) { setState(() => _isVideoLoading = false); @@ -202,6 +208,7 @@ class _PremiumUploadDialogState extends State { ? null : descriptionController.text, tags: tags, + durationSeconds: _videoDurationSeconds, ); if (!mounted) return; diff --git a/lib/providers/videos_provider.dart b/lib/providers/videos_provider.dart index 867cb0d..4a64fc2 100644 --- a/lib/providers/videos_provider.dart +++ b/lib/providers/videos_provider.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:video_player/video_player.dart'; import 'package:path/path.dart' as p; import 'package:energy_media/helpers/globals.dart'; import 'package:energy_media/models/media/media_models.dart'; @@ -171,6 +172,10 @@ class VideosProvider extends ChangeNotifier { value: media.seconds != null ? _formatDuration(media.seconds!) : '-'), + 'file_size': PlutoCell( + value: media.fileSizeBytes != null + ? _formatFileSize(media.fileSizeBytes!) + : '-'), 'createdAt': PlutoCell( value: media.createdAt?.toString().split('.')[0] ?? '-'), 'tags': PlutoCell(value: media.tags.join(', ')), @@ -750,6 +755,90 @@ class VideosProvider extends ChangeNotifier { } } + /// Update missing video durations (batch process) + Future> updateMissingDurations( + Function(int current, int total) onProgress, + ) async { + try { + // Obtener videos sin duración + final videosWithoutDuration = mediaFiles + .where((video) => video.seconds == null && video.fileUrl != null) + .toList(); + + if (videosWithoutDuration.isEmpty) { + return {'success': true, 'updated': 0, 'failed': 0}; + } + + int updated = 0; + int failed = 0; + + for (int i = 0; i < videosWithoutDuration.length; i++) { + final video = videosWithoutDuration[i]; + onProgress(i + 1, videosWithoutDuration.length); + + VideoPlayerController? controller; + try { + // Inicializar VideoPlayerController para obtener duración + controller = VideoPlayerController.network(video.fileUrl!); + await controller.initialize(); + + final durationSeconds = controller.value.duration.inSeconds; + + if (durationSeconds > 0) { + // Obtener metadata actual + final response = await supabaseML + .from('media_files') + .select('metadata_json') + .eq('media_file_id', video.mediaFileId) + .eq('organization_fk', organizationId) + .single(); + + final metadata = + response['metadata_json'] as Map? ?? {}; + + // Actualizar metadata con duración + metadata['duration_seconds'] = durationSeconds; + + // Actualizar tanto seconds como metadata_json + await supabaseML + .from('media_files') + .update({ + 'seconds': durationSeconds, + 'metadata_json': metadata, + }) + .eq('media_file_id', video.mediaFileId) + .eq('organization_fk', organizationId); + + updated++; + print('✅ Video ${video.mediaFileId}: $durationSeconds segundos'); + } else { + failed++; + print('⚠️ Video ${video.mediaFileId}: duración inválida'); + } + } catch (e) { + print('❌ Error procesando video ${video.mediaFileId}: $e'); + failed++; + } finally { + // Limpiar recursos del controller + controller?.dispose(); + } + } + + // Recargar datos + await loadMediaFiles(); + + return { + 'success': true, + 'updated': updated, + 'failed': failed, + 'total': videosWithoutDuration.length, + }; + } catch (e) { + print('Error en updateMissingDurations: $e'); + return {'success': false, 'error': e.toString()}; + } + } + // ========== SEARCH & FILTER ========== /// Search videos by title or description @@ -782,6 +871,10 @@ class VideosProvider extends ChangeNotifier { value: media.seconds != null ? _formatDuration(media.seconds!) : '-'), + 'file_size': PlutoCell( + value: media.fileSizeBytes != null + ? _formatFileSize(media.fileSizeBytes!) + : '-'), 'createdAt': PlutoCell( value: media.createdAt?.toString().split('.')[0] ?? '-'), 'tags': PlutoCell(value: media.tags.join(', ')), diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 5f656e7..26861ca 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -62,6 +62,7 @@ abstract class AppTheme { abstract Color error; abstract Color warning; abstract Color success; + abstract Color info; abstract Color formBackground; Gradient blueGradient = const LinearGradient( @@ -163,6 +164,8 @@ class LightModeTheme extends AppTheme { @override Color success = const Color(0xFF4EC9F5); // Cyan accent @override + Color info = const Color(0xFF3B82F6); // Blue info + @override Color formBackground = const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios @@ -210,6 +213,8 @@ class DarkModeTheme extends AppTheme { @override Color success = const Color(0xFF4EC9F5); // Cyan accent @override + Color info = const Color(0xFF3B82F6); // Blue info + @override Color formBackground = const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios