Tags añadidos
This commit is contained in:
@@ -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<String> get tags =>
|
||||
(metadataJson?['tags'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
@@ -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<GestorVideosPage> {
|
||||
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<GestorVideosPage> {
|
||||
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<bool>(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
@@ -699,6 +744,105 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
||||
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<GestorVideosPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 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(
|
||||
|
||||
@@ -24,6 +24,7 @@ class PremiumUploadDialog extends StatefulWidget {
|
||||
class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
|
||||
final titleController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
final tagsController = TextEditingController();
|
||||
MediaCategoryModel? selectedCategory;
|
||||
Uint8List? selectedVideo;
|
||||
String? videoFileName;
|
||||
@@ -36,6 +37,7 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
|
||||
void dispose() {
|
||||
titleController.dispose();
|
||||
descriptionController.dispose();
|
||||
tagsController.dispose();
|
||||
_videoController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -84,12 +86,23 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
|
||||
|
||||
setState(() => isUploading = true);
|
||||
|
||||
// Procesar tags: separar por comas o espacios
|
||||
List<String>? 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<PremiumUploadDialog> {
|
||||
_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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<String>? 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<bool> updateVideoTags(int mediaFileId, List<String> 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<String, dynamic>? ?? {};
|
||||
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<bool> 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<String, dynamic>? ?? {};
|
||||
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),
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user