Tags añadidos

This commit is contained in:
Abraham
2026-01-12 18:40:28 -08:00
parent a9214e9eac
commit d1271a5578
4 changed files with 305 additions and 1 deletions

View File

@@ -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(

View File

@@ -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,
),
],
);
}