Files
energymedia_content_manager/lib/providers/videos_provider.dart

677 lines
20 KiB
Dart

import 'dart:typed_data';
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:path/path.dart' as p;
import 'package:nethive_neo/helpers/globals.dart';
import 'package:nethive_neo/models/media/media_models.dart';
class VideosProvider extends ChangeNotifier {
// ========== ORGANIZATION CONSTANT ==========
static const int organizationId = 17;
// ========== STATE MANAGEMENT ==========
PlutoGridStateManager? stateManager;
List<PlutoRow> videosRows = [];
// ========== DATA LISTS ==========
List<MediaFileModel> mediaFiles = [];
List<MediaCategoryModel> categories = [];
List<MediaWithPosterModel> mediaWithPosters = [];
// ========== CONTROLLERS ==========
final busquedaVideoController = TextEditingController();
final tituloController = TextEditingController();
final descripcionController = TextEditingController();
// ========== VIDEO/IMAGE UPLOAD STATE ==========
String? videoName;
String? videoUrl;
String? videoStoragePath;
String videoFileExtension = '';
Uint8List? webVideoBytes;
String? posterName;
String? posterUrl;
String? posterStoragePath;
String posterFileExtension = '';
Uint8List? webPosterBytes;
// ========== LOADING STATE ==========
bool isLoading = false;
String? errorMessage;
// ========== CONSTRUCTOR ==========
VideosProvider() {
loadMediaFiles();
loadCategories();
}
// ========== LOAD METHODS ==========
/// Load all media files with organization filter
Future<void> loadMediaFiles() async {
try {
isLoading = true;
errorMessage = null;
notifyListeners();
final response = await supabaseML
.from('media_files')
.select()
.eq('organization_fk', organizationId)
.order('created_at_timestamp', ascending: false);
mediaFiles = (response as List<dynamic>)
.map((item) => MediaFileModel.fromMap(item))
.toList();
await _buildPlutoRows();
isLoading = false;
notifyListeners();
} catch (e) {
errorMessage = 'Error cargando videos: $e';
isLoading = false;
notifyListeners();
print('Error en loadMediaFiles: $e');
}
}
/// Load media files with posters using view
Future<void> loadMediaWithPosters() async {
try {
isLoading = true;
notifyListeners();
final response = await supabaseML
.from('vw_media_files_with_posters')
.select()
.eq('organization_fk', organizationId)
.order('media_created_at', ascending: false);
mediaWithPosters = (response as List<dynamic>)
.map((item) => MediaWithPosterModel.fromMap(item))
.toList();
isLoading = false;
notifyListeners();
} catch (e) {
errorMessage = 'Error cargando videos con posters: $e';
isLoading = false;
notifyListeners();
print('Error en loadMediaWithPosters: $e');
}
}
/// Load all categories
Future<void> loadCategories() async {
try {
final response = await supabaseML
.from('media_categories')
.select()
.order('category_name');
categories = (response as List<dynamic>)
.map((item) => MediaCategoryModel.fromMap(item))
.toList();
notifyListeners();
} catch (e) {
print('Error en loadCategories: $e');
}
}
/// Build PlutoGrid rows from media files
Future<void> _buildPlutoRows() async {
videosRows.clear();
for (var media in mediaFiles) {
videosRows.add(
PlutoRow(
cells: {
'video': PlutoCell(value: media), // Objeto completo para renderers
'thumbnail': PlutoCell(value: media.fileUrl),
'title': PlutoCell(value: media.title ?? media.fileName),
'file_description': PlutoCell(value: media.fileDescription),
'category':
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
'reproducciones': PlutoCell(value: media.reproducciones),
'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';
try {
return categories
.firstWhere((cat) => cat.mediaCategoriesId == categoryId)
.categoryName;
} catch (e) {
return 'Sin categoría';
}
}
/// Format file size to human readable
String _formatFileSize(int? bytes) {
if (bytes == null) return '-';
if (bytes < 1024) return '$bytes B';
if (bytes < 1048576) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1073741824) return '${(bytes / 1048576).toStringAsFixed(1)} MB';
return '${(bytes / 1073741824).toStringAsFixed(1)} GB';
}
// ========== VIDEO UPLOAD ==========
/// Select video file from device
Future<bool> selectVideo() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? pickedVideo = await picker.pickVideo(
source: ImageSource.gallery,
);
if (pickedVideo == null) return false;
videoName = pickedVideo.name;
videoFileExtension = p.extension(pickedVideo.name);
webVideoBytes = await pickedVideo.readAsBytes();
// Remove extension from name for title
final nameWithoutExt = videoName!.replaceAll(videoFileExtension, '');
tituloController.text = nameWithoutExt;
notifyListeners();
return true;
} catch (e) {
errorMessage = 'Error seleccionando video: $e';
notifyListeners();
return false;
}
}
/// Select poster/thumbnail image
Future<bool> selectPoster() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? pickedImage = await picker.pickImage(
source: ImageSource.gallery,
);
if (pickedImage == null) return false;
posterName = pickedImage.name;
posterFileExtension = p.extension(pickedImage.name);
webPosterBytes = await pickedImage.readAsBytes();
notifyListeners();
return true;
} catch (e) {
errorMessage = 'Error seleccionando poster: $e';
notifyListeners();
return false;
}
}
/// Upload video to Supabase Storage and create record
Future<bool> uploadVideo({
required String title,
String? description,
int? categoryId,
int? durationSeconds,
}) async {
if (webVideoBytes == null || videoName == null) {
errorMessage = 'No hay video seleccionado';
notifyListeners();
return false;
}
try {
isLoading = true;
notifyListeners();
// 1. Upload video to storage
final timestamp = DateTime.now().millisecondsSinceEpoch;
final fileName = '${timestamp}_$videoName';
videoStoragePath = 'videos/$fileName';
await supabaseML.storage.from('energymedia').uploadBinary(
videoStoragePath!,
webVideoBytes!,
fileOptions: const FileOptions(
cacheControl: '3600',
upsert: false,
),
);
// 2. Get public URL
videoUrl = supabaseML.storage
.from('energymedia')
.getPublicUrl(videoStoragePath!);
// 3. Upload poster if exists (solo storage, no DB)
String? posterUrlUploaded;
if (webPosterBytes != null && posterName != null) {
posterUrlUploaded = await _uploadPoster();
}
// 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,
};
await supabaseML.from('media_files').insert({
'file_name': fileName,
'title': title,
'file_description': description,
'file_type': 'video',
'mime_type': _getMimeType(videoFileExtension),
'file_extension': videoFileExtension,
'file_size_bytes': webVideoBytes!.length,
'file_url': videoUrl,
'storage_path': videoStoragePath,
'organization_fk': organizationId,
'media_category_fk': categoryId,
'metadata_json': metadataJson,
'seconds': durationSeconds,
'is_public_file': true,
'uploaded_by_user_id': currentUser?.id,
});
// Clean up
_clearUploadState();
// Reload data
await loadMediaFiles();
isLoading = false;
notifyListeners();
return true;
} catch (e) {
errorMessage = 'Error subiendo video: $e';
isLoading = false;
notifyListeners();
print('Error en uploadVideo: $e');
return false;
}
}
/// 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 {
final timestamp = DateTime.now().millisecondsSinceEpoch;
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!,
fileOptions: const FileOptions(
cacheControl: '3600',
upsert: false,
),
);
// Obtener URL pública del poster
posterUrl = supabaseML.storage
.from('energymedia')
.getPublicUrl(posterStoragePath!);
return posterUrl; // Retornar solo la URL, no el ID
} catch (e) {
print('Error en _uploadPoster: $e');
return null;
}
}
/// Get MIME type from file extension
String _getMimeType(String extension) {
final ext = extension.toLowerCase().replaceAll('.', '');
switch (ext) {
case 'mp4':
return 'video/mp4';
case 'webm':
return 'video/webm';
case 'mov':
return 'video/quicktime';
case 'avi':
return 'video/x-msvideo';
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
default:
return 'application/octet-stream';
}
}
/// Clear upload state
void _clearUploadState() {
videoName = null;
videoUrl = null;
videoStoragePath = null;
videoFileExtension = '';
webVideoBytes = null;
posterName = null;
posterUrl = null;
posterStoragePath = null;
posterFileExtension = '';
webPosterBytes = null;
tituloController.clear();
descripcionController.clear();
}
// ========== UPDATE METHODS ==========
/// Update video title
Future<bool> updateVideoTitle(int mediaFileId, String title) async {
try {
await supabaseML
.from('media_files')
.update({'title': title})
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
return true;
} catch (e) {
errorMessage = 'Error actualizando título: $e';
notifyListeners();
print('Error en updateVideoTitle: $e');
return false;
}
}
/// Update video description
Future<bool> updateVideoDescription(
int mediaFileId, String description) async {
try {
await supabaseML
.from('media_files')
.update({'file_description': description})
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
return true;
} catch (e) {
errorMessage = 'Error actualizando descripción: $e';
notifyListeners();
print('Error en updateVideoDescription: $e');
return false;
}
}
/// Update video category
Future<bool> updateVideoCategory(int mediaFileId, int? categoryId) async {
try {
await supabaseML
.from('media_files')
.update({'media_category_fk': categoryId})
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
return true;
} catch (e) {
errorMessage = 'Error actualizando categoría: $e';
notifyListeners();
print('Error en updateVideoCategory: $e');
return false;
}
}
/// Update video metadata
Future<bool> updateVideoMetadata(
int mediaFileId,
Map<String, dynamic> metadata,
) async {
try {
await supabaseML
.from('media_files')
.update({'metadata_json': metadata})
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
return true;
} catch (e) {
errorMessage = 'Error actualizando metadata: $e';
notifyListeners();
print('Error en updateVideoMetadata: $e');
return false;
}
}
// ========== DELETE METHODS ==========
/// Delete video and its storage files
Future<bool> deleteVideo(int mediaFileId) async {
try {
isLoading = true;
notifyListeners();
// Get video info
final response = await supabaseML
.from('media_files')
.select()
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId)
.single();
final storagePath = response['storage_path'] as String?;
final metadataJson = response['metadata_json'] as Map<String, dynamic>?;
// Delete video from storage if path exists
if (storagePath != null) {
await supabaseML.storage.from('energymedia').remove([storagePath]);
}
// 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;
// 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
await supabaseML
.from('media_files')
.delete()
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
isLoading = false;
notifyListeners();
return true;
} catch (e) {
errorMessage = 'Error eliminando video: $e';
isLoading = false;
notifyListeners();
print('Error en deleteVideo: $e');
return false;
}
}
// ========== ANALYTICS METHODS ==========
/// Increment view count
Future<bool> incrementReproduccion(int mediaFileId) async {
try {
// Get current metadata
final response = await supabaseML
.from('media_files')
.select('metadata_json')
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId)
.single();
final metadata = response['metadata_json'] as Map<String, dynamic>? ?? {};
final currentCount = metadata['reproducciones'] ?? 0;
metadata['reproducciones'] = currentCount + 1;
metadata['last_viewed_at'] = DateTime.now().toIso8601String();
await updateVideoMetadata(mediaFileId, metadata);
return true;
} catch (e) {
print('Error en incrementReproduccion: $e');
return false;
}
}
/// Get dashboard statistics
Future<Map<String, dynamic>> getDashboardStats() async {
try {
// Total videos
final totalVideos = mediaFiles.length;
// Total reproducciones
int totalReproducciones = 0;
for (var media in mediaFiles) {
totalReproducciones += media.reproducciones;
}
// Most viewed video
MediaFileModel? mostViewed;
if (mediaFiles.isNotEmpty) {
mostViewed = mediaFiles.reduce((curr, next) =>
curr.reproducciones > next.reproducciones ? curr : next);
}
// Videos by category
Map<String, int> videosByCategory = {};
for (var media in mediaFiles) {
final categoryName = _getCategoryName(media.mediaCategoryFk);
videosByCategory[categoryName] =
(videosByCategory[categoryName] ?? 0) + 1;
}
// Most viewed category
String? mostViewedCategory;
if (videosByCategory.isNotEmpty) {
mostViewedCategory = videosByCategory.entries
.reduce((a, b) => a.value > b.value ? a : b)
.key;
}
return {
'total_videos': totalVideos,
'total_reproducciones': totalReproducciones,
'most_viewed_video': mostViewed?.toMap(),
'videos_by_category': videosByCategory,
'most_viewed_category': mostViewedCategory,
'total_categories': categories.length,
};
} catch (e) {
print('Error en getDashboardStats: $e');
return {};
}
}
// ========== SEARCH & FILTER ==========
/// Search videos by title or description
void searchVideos(String query) {
if (query.isEmpty) {
_buildPlutoRows();
notifyListeners();
return;
}
videosRows.clear();
final filteredMedia = mediaFiles.where((media) {
final title = (media.title ?? media.fileName).toLowerCase();
final description = (media.fileDescription ?? '').toLowerCase();
final searchQuery = query.toLowerCase();
return title.contains(searchQuery) || description.contains(searchQuery);
}).toList();
for (var media in filteredMedia) {
videosRows.add(
PlutoRow(
cells: {
'video': PlutoCell(value: media),
'thumbnail': PlutoCell(value: media.fileUrl),
'title': PlutoCell(value: media.title ?? media.fileName),
'fileName': PlutoCell(value: media.fileName),
'category':
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
'reproducciones': PlutoCell(value: media.reproducciones),
'duration': PlutoCell(
value: media.seconds != null
? _formatDuration(media.seconds!)
: '-'),
'createdAt': PlutoCell(
value: media.createdAt?.toString().split('.')[0] ?? '-'),
'actions': PlutoCell(value: media),
},
),
);
}
notifyListeners();
}
// ========== CLEANUP ==========
@override
void dispose() {
busquedaVideoController.dispose();
tituloController.dispose();
descripcionController.dispose();
super.dispose();
}
}