From d1271a55783b325c5ee1daa628fa5d2a91326f49 Mon Sep 17 00:00:00 2001 From: Abraham Date: Mon, 12 Jan 2026 18:40:28 -0800 Subject: [PATCH] =?UTF-8?q?Tags=20a=C3=B1adidos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/models/media/media_file_model.dart | 7 + lib/pages/videos/gestor_videos_page.dart | 167 ++++++++++++++++++ .../videos/widgets/premium_upload_dialog.dart | 30 ++++ lib/providers/videos_provider.dart | 102 ++++++++++- 4 files changed, 305 insertions(+), 1 deletion(-) diff --git a/lib/models/media/media_file_model.dart b/lib/models/media/media_file_model.dart index e8e45d6..9012ad4 100644 --- a/lib/models/media/media_file_model.dart +++ b/lib/models/media/media_file_model.dart @@ -158,4 +158,11 @@ class MediaFileModel { String? get posterUrl => metadataJson?['poster_url']; String? get posterFileName => metadataJson?['poster_file_name']; int? get fileSizeBytesFromMetadata => metadataJson?['file_size_bytes']; + + // Tags from metadata_json + List get tags => + (metadataJson?['tags'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + []; } diff --git a/lib/pages/videos/gestor_videos_page.dart b/lib/pages/videos/gestor_videos_page.dart index 6d95bcf..7421d1c 100644 --- a/lib/pages/videos/gestor_videos_page.dart +++ b/lib/pages/videos/gestor_videos_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:provider/provider.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:nethive_neo/providers/videos_provider.dart'; import 'package:nethive_neo/models/media/media_models.dart'; import 'package:nethive_neo/theme/theme.dart'; @@ -319,6 +321,45 @@ class _GestorVideosPageState extends State { type: PlutoColumnType.text(), width: 150, ), + PlutoColumn( + title: 'Etiquetas', + field: 'tags', + type: PlutoColumnType.text(), + width: 180, + renderer: (rendererContext) { + final video = + rendererContext.row.cells['video']?.value as MediaFileModel?; + if (video == null || video.tags.isEmpty) return const SizedBox(); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: Wrap( + spacing: 4, + runSpacing: 4, + children: video.tags.take(3).map((tag) { + return Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFFFFB733)], + ), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + tag, + style: const TextStyle( + color: Color(0xFF0B0B0D), + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ); + }).toList(), + ), + ); + }, + ), PlutoColumn( title: 'Acciones', field: 'actions', @@ -616,10 +657,14 @@ class _GestorVideosPageState extends State { final titleController = TextEditingController(text: video.title); final descriptionController = TextEditingController(text: video.fileDescription); + final tagsController = TextEditingController(text: video.tags.join(', ')); MediaCategoryModel? selectedCategory = provider.categories .where((cat) => cat.mediaCategoriesId == video.mediaCategoryFk) .firstOrNull; + Uint8List? newPosterBytes; + String? newPosterFileName; + final result = await showDialog( context: context, builder: (context) => StatefulBuilder( @@ -699,6 +744,105 @@ class _GestorVideosPageState extends State { setDialogState(() => selectedCategory = value); }, ), + const Gap(16), + TextFormField( + controller: tagsController, + decoration: InputDecoration( + labelText: 'Etiquetas (Tags)', + hintText: 'Ej: deportes, fútbol, entrenamiento', + helperText: 'Separa las etiquetas con comas o espacios', + prefixIcon: Icon( + Icons.label, + color: AppTheme.of(context).primaryColor, + ), + filled: true, + fillColor: AppTheme.of(context).tertiaryBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const Gap(24), + // Sección de portada + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Portada Actual', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + ), + ), + const Gap(12), + if (video.posterUrl != null) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + video.posterUrl!, + height: 120, + width: double.infinity, + fit: BoxFit.cover, + ), + ) + else + Container( + height: 120, + width: double.infinity, + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.video_library, + size: 48, + color: AppTheme.of(context).secondaryText, + ), + ), + const Gap(12), + ElevatedButton.icon( + onPressed: () async { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage( + source: ImageSource.gallery, + maxWidth: 1920, + imageQuality: 85, + ); + + if (image != null) { + final bytes = await image.readAsBytes(); + setDialogState(() { + newPosterBytes = bytes; + newPosterFileName = image.name; + }); + } + }, + icon: const Icon(Icons.image), + label: Text(newPosterBytes != null + ? 'Portada Seleccionada' + : 'Cambiar Portada'), + style: ElevatedButton.styleFrom( + backgroundColor: newPosterBytes != null + ? AppTheme.of(context).alternate + : AppTheme.of(context).primaryColor, + foregroundColor: const Color(0xFF0B0B0D), + ), + ), + if (newPosterBytes != null) ...[ + const Gap(12), + Text( + 'Nueva portada: $newPosterFileName', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).alternate, + fontSize: 12, + ), + ), + ], + ], + ), ], ), ), @@ -739,6 +883,29 @@ class _GestorVideosPageState extends State { ); } + // Actualizar tags + final newTags = tagsController.text + .split(RegExp(r'[,\s]+')) + .map((tag) => tag.trim()) + .where((tag) => tag.isNotEmpty) + .toList(); + + if (newTags.join(',') != video.tags.join(',')) { + await provider.updateVideoTags( + video.mediaFileId, + newTags, + ); + } + + // Actualizar portada si se seleccionó una nueva + if (newPosterBytes != null && newPosterFileName != null) { + await provider.updateVideoPoster( + video.mediaFileId, + newPosterBytes!, + newPosterFileName!, + ); + } + Navigator.pop(context, true); }, style: ElevatedButton.styleFrom( diff --git a/lib/pages/videos/widgets/premium_upload_dialog.dart b/lib/pages/videos/widgets/premium_upload_dialog.dart index 3792ea0..b05a3d7 100644 --- a/lib/pages/videos/widgets/premium_upload_dialog.dart +++ b/lib/pages/videos/widgets/premium_upload_dialog.dart @@ -24,6 +24,7 @@ class PremiumUploadDialog extends StatefulWidget { class _PremiumUploadDialogState extends State { final titleController = TextEditingController(); final descriptionController = TextEditingController(); + final tagsController = TextEditingController(); MediaCategoryModel? selectedCategory; Uint8List? selectedVideo; String? videoFileName; @@ -36,6 +37,7 @@ class _PremiumUploadDialogState extends State { void dispose() { titleController.dispose(); descriptionController.dispose(); + tagsController.dispose(); _videoController?.dispose(); super.dispose(); } @@ -84,12 +86,23 @@ class _PremiumUploadDialogState extends State { setState(() => isUploading = true); + // Procesar tags: separar por comas o espacios + List? tags; + if (tagsController.text.isNotEmpty) { + tags = tagsController.text + .split(RegExp(r'[,\s]+')) // Separar por comas o espacios + .map((tag) => tag.trim()) + .where((tag) => tag.isNotEmpty) + .toList(); + } + final success = await widget.provider.uploadVideo( title: titleController.text, description: descriptionController.text.isEmpty ? null : descriptionController.text, categoryId: selectedCategory!.mediaCategoriesId, + tags: tags, ); if (!mounted) return; @@ -285,6 +298,23 @@ class _PremiumUploadDialogState extends State { _buildLabel('Categoría *'), const Gap(8), _buildCategoryDropdown(), + const Gap(20), + _buildLabel('Etiquetas (Tags)'), + const Gap(4), + Text( + 'Separa las etiquetas con comas o espacios', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + ), + ), + const Gap(8), + _buildTextField( + controller: tagsController, + hintText: 'Ej: deportes, fútbol, entrenamiento', + prefixIcon: Icons.label, + ), ], ); } diff --git a/lib/providers/videos_provider.dart b/lib/providers/videos_provider.dart index fc3d81f..3c42991 100644 --- a/lib/providers/videos_provider.dart +++ b/lib/providers/videos_provider.dart @@ -144,6 +144,7 @@ class VideosProvider extends ChangeNotifier { : '-'), 'createdAt': PlutoCell( value: media.createdAt?.toString().split('.')[0] ?? '-'), + 'tags': PlutoCell(value: media.tags.join(', ')), 'actions': PlutoCell(value: media), }, ), @@ -246,6 +247,7 @@ class VideosProvider extends ChangeNotifier { String? description, int? categoryId, int? durationSeconds, + List? tags, }) async { if (webVideoBytes == null || videoName == null) { errorMessage = 'No hay video seleccionado'; @@ -291,6 +293,7 @@ class VideosProvider extends ChangeNotifier { 'file_size_bytes': webVideoBytes!.length, // Peso del video if (posterUrlUploaded != null) 'poster_url': posterUrlUploaded, if (posterUrlUploaded != null) 'poster_file_name': posterName, + if (tags != null && tags.isNotEmpty) 'tags': tags, }; await supabaseML.from('media_files').insert({ @@ -483,6 +486,102 @@ class VideosProvider extends ChangeNotifier { } } + /// Update video tags + Future updateVideoTags(int mediaFileId, List tags) 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? ?? {}; + metadata['tags'] = tags; + + await updateVideoMetadata(mediaFileId, metadata); + return true; + } catch (e) { + errorMessage = 'Error actualizando tags: $e'; + notifyListeners(); + print('Error en updateVideoTags: $e'); + return false; + } + } + + /// Update video poster + Future updateVideoPoster( + int mediaFileId, Uint8List posterBytes, String posterName) async { + try { + isLoading = true; + notifyListeners(); + + // Upload new poster to storage + final timestamp = DateTime.now().millisecondsSinceEpoch; + final fileName = '${timestamp}_$posterName'; + final posterPath = 'imagenes/$fileName'; + + await supabaseML.storage.from('energymedia').uploadBinary( + posterPath, + posterBytes, + fileOptions: const FileOptions( + cacheControl: '3600', + upsert: false, + ), + ); + + // Get public URL + final newPosterUrl = + supabaseML.storage.from('energymedia').getPublicUrl(posterPath); + + // Get current metadata and old poster URL + 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? ?? {}; + final oldPosterUrl = metadata['poster_url'] as String?; + + // Delete old poster from storage if exists + if (oldPosterUrl != null && oldPosterUrl.isNotEmpty) { + try { + final uri = Uri.parse(oldPosterUrl); + final pathSegments = uri.pathSegments; + final bucketIndex = pathSegments.indexOf('energymedia'); + if (bucketIndex != -1 && bucketIndex < pathSegments.length - 1) { + final oldPosterPath = + pathSegments.sublist(bucketIndex + 1).join('/'); + await supabaseML.storage + .from('energymedia') + .remove([oldPosterPath]); + } + } catch (e) { + print('Error eliminando poster antiguo: $e'); + } + } + + // Update metadata with new poster info + metadata['poster_url'] = newPosterUrl; + metadata['poster_file_name'] = posterName; + + await updateVideoMetadata(mediaFileId, metadata); + + isLoading = false; + notifyListeners(); + return true; + } catch (e) { + errorMessage = 'Error actualizando poster: $e'; + isLoading = false; + notifyListeners(); + print('Error en updateVideoPoster: $e'); + return false; + } + } + // ========== DELETE METHODS ========== /// Delete video and its storage files @@ -645,7 +744,7 @@ class VideosProvider extends ChangeNotifier { 'video': PlutoCell(value: media), 'thumbnail': PlutoCell(value: media.fileUrl), 'title': PlutoCell(value: media.title ?? media.fileName), - 'fileName': PlutoCell(value: media.fileName), + 'file_description': PlutoCell(value: media.fileDescription), 'category': PlutoCell(value: _getCategoryName(media.mediaCategoryFk)), 'reproducciones': PlutoCell(value: media.reproducciones), @@ -655,6 +754,7 @@ class VideosProvider extends ChangeNotifier { : '-'), 'createdAt': PlutoCell( value: media.createdAt?.toString().split('.')[0] ?? '-'), + 'tags': PlutoCell(value: media.tags.join(', ')), 'actions': PlutoCell(value: media), }, ),