boton temporal para actualizar duraciones, fix de duracion en y peso de el archvio

This commit is contained in:
Abraham
2026-01-15 22:50:14 -08:00
parent 0b0d9a81e2
commit 974282ef1e
6 changed files with 456 additions and 3 deletions

View File

@@ -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/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/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/gestor_videos_widgets/delete_video_dialog.dart';
import 'package:energy_media/pages/videos/widgets/video_thumbnail_widget.dart'; import 'package:energy_media/pages/videos/widgets/video_thumbnail_widget.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@@ -437,7 +438,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
title: 'Descripción', title: 'Descripción',
field: 'file_description', field: 'file_description',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 425, width: 400,
enableEditingMode: false, enableEditingMode: false,
renderer: (rendererContext) { renderer: (rendererContext) {
final description = rendererContext.cell.value?.toString() ?? ''; final description = rendererContext.cell.value?.toString() ?? '';
@@ -523,14 +524,105 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
title: 'Duración', title: 'Duración',
field: 'duration', field: 'duration',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 180, width: 120,
enableEditingMode: false, 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( PlutoColumn(
title: 'Fecha de Creación', title: 'Fecha de Creación',
field: 'createdAt', field: 'createdAt',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 280, width: 240,
enableEditingMode: false, enableEditingMode: false,
renderer: (rendererContext) { renderer: (rendererContext) {
final video = final video =

View File

@@ -9,6 +9,7 @@ import 'package:gap/gap.dart';
import 'package:energy_media/theme/theme.dart'; import 'package:energy_media/theme/theme.dart';
import 'package:energy_media/models/media/media_models.dart'; import 'package:energy_media/models/media/media_models.dart';
import 'package:energy_media/providers/videos_provider.dart'; import 'package:energy_media/providers/videos_provider.dart';
import 'package:energy_media/helpers/globals.dart';
class EditVideoDialog extends StatefulWidget { class EditVideoDialog extends StatefulWidget {
final MediaFileModel video; final MediaFileModel video;
@@ -93,6 +94,17 @@ class _EditVideoDialogState extends State<EditVideoDialog> {
), ),
); );
// 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); setState(() => _isVideoLoading = false);
} catch (e) { } catch (e) {
setState(() => _isVideoLoading = false); setState(() => _isVideoLoading = false);
@@ -160,6 +172,33 @@ class _EditVideoDialogState extends State<EditVideoDialog> {
} }
} }
/// Guardar duración capturada en la base de datos
Future<void> _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<String, dynamic>? ?? {};
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<void> _saveChanges() async { Future<void> _saveChanges() async {
// Actualizar título // Actualizar título
if (titleController.text != widget.video.title) { if (titleController.text != widget.video.title) {

View File

@@ -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<VideosProvider>(
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<void> _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<void> _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<Color>(
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,
),
],
],
),
),
);
}
}

View File

@@ -38,6 +38,7 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
bool isUploading = false; bool isUploading = false;
bool _isVideoLoading = false; bool _isVideoLoading = false;
String? _videoBlobUrl; String? _videoBlobUrl;
int? _videoDurationSeconds; // Duración capturada del video
@override @override
void dispose() { void dispose() {
@@ -108,6 +109,11 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
), ),
); );
// Capturar duración del video
_videoDurationSeconds = _videoController!.value.duration.inSeconds;
debugPrint(
'🕒 Duración del video capturada: $_videoDurationSeconds segundos');
setState(() => _isVideoLoading = false); setState(() => _isVideoLoading = false);
} catch (e) { } catch (e) {
setState(() => _isVideoLoading = false); setState(() => _isVideoLoading = false);
@@ -202,6 +208,7 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
? null ? null
: descriptionController.text, : descriptionController.text,
tags: tags, tags: tags,
durationSeconds: _videoDurationSeconds,
); );
if (!mounted) return; if (!mounted) return;

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:pluto_grid/pluto_grid.dart'; import 'package:pluto_grid/pluto_grid.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:video_player/video_player.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:energy_media/helpers/globals.dart'; import 'package:energy_media/helpers/globals.dart';
import 'package:energy_media/models/media/media_models.dart'; import 'package:energy_media/models/media/media_models.dart';
@@ -171,6 +172,10 @@ class VideosProvider extends ChangeNotifier {
value: media.seconds != null value: media.seconds != null
? _formatDuration(media.seconds!) ? _formatDuration(media.seconds!)
: '-'), : '-'),
'file_size': PlutoCell(
value: media.fileSizeBytes != null
? _formatFileSize(media.fileSizeBytes!)
: '-'),
'createdAt': PlutoCell( 'createdAt': PlutoCell(
value: media.createdAt?.toString().split('.')[0] ?? '-'), value: media.createdAt?.toString().split('.')[0] ?? '-'),
'tags': PlutoCell(value: media.tags.join(', ')), 'tags': PlutoCell(value: media.tags.join(', ')),
@@ -750,6 +755,90 @@ class VideosProvider extends ChangeNotifier {
} }
} }
/// Update missing video durations (batch process)
Future<Map<String, dynamic>> 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<String, dynamic>? ?? {};
// 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 & FILTER ==========
/// Search videos by title or description /// Search videos by title or description
@@ -782,6 +871,10 @@ class VideosProvider extends ChangeNotifier {
value: media.seconds != null value: media.seconds != null
? _formatDuration(media.seconds!) ? _formatDuration(media.seconds!)
: '-'), : '-'),
'file_size': PlutoCell(
value: media.fileSizeBytes != null
? _formatFileSize(media.fileSizeBytes!)
: '-'),
'createdAt': PlutoCell( 'createdAt': PlutoCell(
value: media.createdAt?.toString().split('.')[0] ?? '-'), value: media.createdAt?.toString().split('.')[0] ?? '-'),
'tags': PlutoCell(value: media.tags.join(', ')), 'tags': PlutoCell(value: media.tags.join(', ')),

View File

@@ -62,6 +62,7 @@ abstract class AppTheme {
abstract Color error; abstract Color error;
abstract Color warning; abstract Color warning;
abstract Color success; abstract Color success;
abstract Color info;
abstract Color formBackground; abstract Color formBackground;
Gradient blueGradient = const LinearGradient( Gradient blueGradient = const LinearGradient(
@@ -163,6 +164,8 @@ class LightModeTheme extends AppTheme {
@override @override
Color success = const Color(0xFF4EC9F5); // Cyan accent Color success = const Color(0xFF4EC9F5); // Cyan accent
@override @override
Color info = const Color(0xFF3B82F6); // Blue info
@override
Color formBackground = Color formBackground =
const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios
@@ -210,6 +213,8 @@ class DarkModeTheme extends AppTheme {
@override @override
Color success = const Color(0xFF4EC9F5); // Cyan accent Color success = const Color(0xFF4EC9F5); // Cyan accent
@override @override
Color info = const Color(0xFF3B82F6); // Blue info
@override
Color formBackground = Color formBackground =
const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios