Tags añadidos
This commit is contained in:
@@ -158,4 +158,11 @@ class MediaFileModel {
|
|||||||
String? get posterUrl => metadataJson?['poster_url'];
|
String? get posterUrl => metadataJson?['poster_url'];
|
||||||
String? get posterFileName => metadataJson?['poster_file_name'];
|
String? get posterFileName => metadataJson?['poster_file_name'];
|
||||||
int? get fileSizeBytesFromMetadata => metadataJson?['file_size_bytes'];
|
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/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:pluto_grid/pluto_grid.dart';
|
import 'package:pluto_grid/pluto_grid.dart';
|
||||||
import 'package:provider/provider.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/providers/videos_provider.dart';
|
||||||
import 'package:nethive_neo/models/media/media_models.dart';
|
import 'package:nethive_neo/models/media/media_models.dart';
|
||||||
import 'package:nethive_neo/theme/theme.dart';
|
import 'package:nethive_neo/theme/theme.dart';
|
||||||
@@ -319,6 +321,45 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
type: PlutoColumnType.text(),
|
type: PlutoColumnType.text(),
|
||||||
width: 150,
|
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(
|
PlutoColumn(
|
||||||
title: 'Acciones',
|
title: 'Acciones',
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
@@ -616,10 +657,14 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
final titleController = TextEditingController(text: video.title);
|
final titleController = TextEditingController(text: video.title);
|
||||||
final descriptionController =
|
final descriptionController =
|
||||||
TextEditingController(text: video.fileDescription);
|
TextEditingController(text: video.fileDescription);
|
||||||
|
final tagsController = TextEditingController(text: video.tags.join(', '));
|
||||||
MediaCategoryModel? selectedCategory = provider.categories
|
MediaCategoryModel? selectedCategory = provider.categories
|
||||||
.where((cat) => cat.mediaCategoriesId == video.mediaCategoryFk)
|
.where((cat) => cat.mediaCategoriesId == video.mediaCategoryFk)
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
|
|
||||||
|
Uint8List? newPosterBytes;
|
||||||
|
String? newPosterFileName;
|
||||||
|
|
||||||
final result = await showDialog<bool>(
|
final result = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
@@ -699,6 +744,105 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
setDialogState(() => selectedCategory = value);
|
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);
|
Navigator.pop(context, true);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class PremiumUploadDialog extends StatefulWidget {
|
|||||||
class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
|
class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
|
||||||
final titleController = TextEditingController();
|
final titleController = TextEditingController();
|
||||||
final descriptionController = TextEditingController();
|
final descriptionController = TextEditingController();
|
||||||
|
final tagsController = TextEditingController();
|
||||||
MediaCategoryModel? selectedCategory;
|
MediaCategoryModel? selectedCategory;
|
||||||
Uint8List? selectedVideo;
|
Uint8List? selectedVideo;
|
||||||
String? videoFileName;
|
String? videoFileName;
|
||||||
@@ -36,6 +37,7 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
titleController.dispose();
|
titleController.dispose();
|
||||||
descriptionController.dispose();
|
descriptionController.dispose();
|
||||||
|
tagsController.dispose();
|
||||||
_videoController?.dispose();
|
_videoController?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -84,12 +86,23 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
|
|||||||
|
|
||||||
setState(() => isUploading = true);
|
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(
|
final success = await widget.provider.uploadVideo(
|
||||||
title: titleController.text,
|
title: titleController.text,
|
||||||
description: descriptionController.text.isEmpty
|
description: descriptionController.text.isEmpty
|
||||||
? null
|
? null
|
||||||
: descriptionController.text,
|
: descriptionController.text,
|
||||||
categoryId: selectedCategory!.mediaCategoriesId,
|
categoryId: selectedCategory!.mediaCategoriesId,
|
||||||
|
tags: tags,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -285,6 +298,23 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
|
|||||||
_buildLabel('Categoría *'),
|
_buildLabel('Categoría *'),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
_buildCategoryDropdown(),
|
_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(
|
'createdAt': PlutoCell(
|
||||||
value: media.createdAt?.toString().split('.')[0] ?? '-'),
|
value: media.createdAt?.toString().split('.')[0] ?? '-'),
|
||||||
|
'tags': PlutoCell(value: media.tags.join(', ')),
|
||||||
'actions': PlutoCell(value: media),
|
'actions': PlutoCell(value: media),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -246,6 +247,7 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
String? description,
|
String? description,
|
||||||
int? categoryId,
|
int? categoryId,
|
||||||
int? durationSeconds,
|
int? durationSeconds,
|
||||||
|
List<String>? tags,
|
||||||
}) async {
|
}) async {
|
||||||
if (webVideoBytes == null || videoName == null) {
|
if (webVideoBytes == null || videoName == null) {
|
||||||
errorMessage = 'No hay video seleccionado';
|
errorMessage = 'No hay video seleccionado';
|
||||||
@@ -291,6 +293,7 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
'file_size_bytes': webVideoBytes!.length, // Peso del video
|
'file_size_bytes': webVideoBytes!.length, // Peso del video
|
||||||
if (posterUrlUploaded != null) 'poster_url': posterUrlUploaded,
|
if (posterUrlUploaded != null) 'poster_url': posterUrlUploaded,
|
||||||
if (posterUrlUploaded != null) 'poster_file_name': posterName,
|
if (posterUrlUploaded != null) 'poster_file_name': posterName,
|
||||||
|
if (tags != null && tags.isNotEmpty) 'tags': tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
await supabaseML.from('media_files').insert({
|
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 METHODS ==========
|
||||||
|
|
||||||
/// Delete video and its storage files
|
/// Delete video and its storage files
|
||||||
@@ -645,7 +744,7 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
'video': PlutoCell(value: media),
|
'video': PlutoCell(value: media),
|
||||||
'thumbnail': PlutoCell(value: media.fileUrl),
|
'thumbnail': PlutoCell(value: media.fileUrl),
|
||||||
'title': PlutoCell(value: media.title ?? media.fileName),
|
'title': PlutoCell(value: media.title ?? media.fileName),
|
||||||
'fileName': PlutoCell(value: media.fileName),
|
'file_description': PlutoCell(value: media.fileDescription),
|
||||||
'category':
|
'category':
|
||||||
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
|
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
|
||||||
'reproducciones': PlutoCell(value: media.reproducciones),
|
'reproducciones': PlutoCell(value: media.reproducciones),
|
||||||
@@ -655,6 +754,7 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
: '-'),
|
: '-'),
|
||||||
'createdAt': PlutoCell(
|
'createdAt': PlutoCell(
|
||||||
value: media.createdAt?.toString().split('.')[0] ?? '-'),
|
value: media.createdAt?.toString().split('.')[0] ?? '-'),
|
||||||
|
'tags': PlutoCell(value: media.tags.join(', ')),
|
||||||
'actions': PlutoCell(value: media),
|
'actions': PlutoCell(value: media),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user