fix gestor de contenido, wip reproductor y diseño

This commit is contained in:
Abraham
2026-01-12 16:36:47 -08:00
parent 1a1049b653
commit 854a0940ae
11 changed files with 159 additions and 222 deletions

View File

@@ -131,24 +131,42 @@ class VideosProvider extends ChangeNotifier {
videosRows.add(
PlutoRow(
cells: {
'id': PlutoCell(value: media.mediaFileId),
'thumbnail':
PlutoCell(value: media.fileUrl), // Para mostrar thumbnail
'video': PlutoCell(value: media), // Objeto completo para renderers
'thumbnail': PlutoCell(value: media.fileUrl),
'title': PlutoCell(value: media.title ?? media.fileName),
'description': PlutoCell(value: media.fileDescription ?? ''),
'file_description': PlutoCell(value: media.fileDescription),
'category':
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
'reproducciones': PlutoCell(value: media.reproducciones),
'duration': PlutoCell(value: media.seconds ?? 0),
'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)),
'created_at': PlutoCell(value: media.createdAt),
'actions': PlutoCell(value: media.mediaFileId),
'duration': PlutoCell(
value: media.seconds != null
? _formatDuration(media.seconds!)
: '-'),
'createdAt': PlutoCell(
value: media.createdAt?.toString().split('.')[0] ?? '-'),
'actions': PlutoCell(value: media),
},
),
);
}
}
/// Format duration in seconds to human readable
String _formatDuration(int seconds) {
final duration = Duration(seconds: seconds);
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final secs = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours}h ${minutes}m';
} else if (minutes > 0) {
return '${minutes}m ${secs}s';
} else {
return '${secs}s';
}
}
/// Get category name by ID
String _getCategoryName(int? categoryId) {
if (categoryId == null) return 'Sin categoría';
@@ -258,21 +276,24 @@ class VideosProvider extends ChangeNotifier {
.from('energymedia')
.getPublicUrl(videoStoragePath!);
// 3. Upload poster if exists
int? posterFileId;
// 3. Upload poster if exists (solo storage, no DB)
String? posterUrlUploaded;
if (webPosterBytes != null && posterName != null) {
posterFileId = await _uploadPoster();
posterUrlUploaded = await _uploadPoster();
}
// 4. Create media_files record
// 4. Create media_files record (UN SOLO REGISTRO con poster en metadata_json)
final metadataJson = {
'uploaded_at': DateTime.now().toIso8601String(),
'reproducciones': 0,
'original_file_name': videoName,
'duration_seconds': durationSeconds,
'file_size_bytes': webVideoBytes!.length, // Peso del video
if (posterUrlUploaded != null) 'poster_url': posterUrlUploaded,
if (posterUrlUploaded != null) 'poster_file_name': posterName,
};
final response = await supabaseML.from('media_files').insert({
await supabaseML.from('media_files').insert({
'file_name': fileName,
'title': title,
'file_description': description,
@@ -288,16 +309,7 @@ class VideosProvider extends ChangeNotifier {
'seconds': durationSeconds,
'is_public_file': true,
'uploaded_by_user_id': currentUser?.id,
}).select();
// 5. Create poster relationship if exists
if (posterFileId != null && response.isNotEmpty) {
final mediaFileId = response[0]['media_file_id'];
await supabaseML.from('media_posters').insert({
'media_file_id': mediaFileId,
'poster_file_id': posterFileId,
});
}
});
// Clean up
_clearUploadState();
@@ -317,8 +329,9 @@ class VideosProvider extends ChangeNotifier {
}
}
/// Upload poster image (internal helper)
Future<int?> _uploadPoster() async {
/// Upload poster image to storage only (NO database record)
/// Returns the public URL of the uploaded poster
Future<String?> _uploadPoster() async {
if (webPosterBytes == null || posterName == null) return null;
try {
@@ -326,6 +339,7 @@ class VideosProvider extends ChangeNotifier {
final fileName = '${timestamp}_$posterName';
posterStoragePath = 'imagenes/$fileName';
// Solo subir al storage, NO crear registro en media_files
await supabaseML.storage.from('energymedia').uploadBinary(
posterStoragePath!,
webPosterBytes!,
@@ -335,26 +349,12 @@ class VideosProvider extends ChangeNotifier {
),
);
// Obtener URL pública del poster
posterUrl = supabaseML.storage
.from('energymedia')
.getPublicUrl(posterStoragePath!);
// Create media_files record for poster
final response = await supabaseML.from('media_files').insert({
'file_name': fileName,
'title': 'Poster',
'file_type': 'image',
'mime_type': _getMimeType(posterFileExtension),
'file_extension': posterFileExtension,
'file_size_bytes': webPosterBytes!.length,
'file_url': posterUrl,
'storage_path': posterStoragePath,
'organization_fk': organizationId,
'is_public_file': true,
'uploaded_by_user_id': currentUser?.id,
}).select();
return response[0]['media_file_id'] as int;
return posterUrl; // Retornar solo la URL, no el ID
} catch (e) {
print('Error en _uploadPoster: $e');
return null;
@@ -500,23 +500,30 @@ class VideosProvider extends ChangeNotifier {
.single();
final storagePath = response['storage_path'] as String?;
final metadataJson = response['metadata_json'] as Map<String, dynamic>?;
// Delete from storage if path exists
// Delete video from storage if path exists
if (storagePath != null) {
await supabaseML.storage.from('energymedia').remove([storagePath]);
}
// Delete associated posters
final posters = await supabaseML
.from('media_posters')
.select('poster_file_id')
.eq('media_file_id', mediaFileId);
// Delete poster from storage if exists in metadata_json
if (metadataJson != null && metadataJson['poster_url'] != null) {
final posterUrl = metadataJson['poster_url'] as String;
// Extraer el path del storage desde la URL
// URL format: https://xxx.supabase.co/storage/v1/object/public/energymedia/imagenes/filename.png
final uri = Uri.parse(posterUrl);
final pathSegments = uri.pathSegments;
for (var poster in posters) {
await _deletePosterFile(poster['poster_file_id']);
// Encontrar el índice después de 'energymedia' y construir el path
final bucketIndex = pathSegments.indexOf('energymedia');
if (bucketIndex != -1 && bucketIndex < pathSegments.length - 1) {
final posterPath = pathSegments.sublist(bucketIndex + 1).join('/');
await supabaseML.storage.from('energymedia').remove([posterPath]);
}
}
// Delete database record (cascade will delete posters relationship)
// Delete database record
await supabaseML
.from('media_files')
.delete()
@@ -537,30 +544,6 @@ class VideosProvider extends ChangeNotifier {
}
}
/// Delete poster file (internal helper)
Future<void> _deletePosterFile(int posterFileId) async {
try {
final response = await supabaseML
.from('media_files')
.select('storage_path')
.eq('media_file_id', posterFileId)
.single();
final storagePath = response['storage_path'] as String?;
if (storagePath != null) {
await supabaseML.storage.from('energymedia').remove([storagePath]);
}
await supabaseML
.from('media_files')
.delete()
.eq('media_file_id', posterFileId);
} catch (e) {
print('Error en _deletePosterFile: $e');
}
}
// ========== ANALYTICS METHODS ==========
/// Increment view count
@@ -659,17 +642,20 @@ class VideosProvider extends ChangeNotifier {
videosRows.add(
PlutoRow(
cells: {
'id': PlutoCell(value: media.mediaFileId),
'video': PlutoCell(value: media),
'thumbnail': PlutoCell(value: media.fileUrl),
'title': PlutoCell(value: media.title ?? media.fileName),
'description': PlutoCell(value: media.fileDescription ?? ''),
'fileName': PlutoCell(value: media.fileName),
'category':
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
'reproducciones': PlutoCell(value: media.reproducciones),
'duration': PlutoCell(value: media.seconds ?? 0),
'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)),
'created_at': PlutoCell(value: media.createdAt),
'actions': PlutoCell(value: media.mediaFileId),
'duration': PlutoCell(
value: media.seconds != null
? _formatDuration(media.seconds!)
: '-'),
'createdAt': PlutoCell(
value: media.createdAt?.toString().split('.')[0] ?? '-'),
'actions': PlutoCell(value: media),
},
),
);