boton temporal para actualizar duraciones, fix de duracion en y peso de el archvio
This commit is contained in:
@@ -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 =
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(', ')),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user