import 'package:flutter/material.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:provider/provider.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'; import 'package:nethive_neo/helpers/globals.dart'; import 'package:nethive_neo/widgets/premium_button.dart'; import 'package:nethive_neo/pages/videos/widgets/premium_upload_dialog.dart'; import 'package:gap/gap.dart'; class GestorVideosPage extends StatefulWidget { const GestorVideosPage({Key? key}) : super(key: key); @override State createState() => _GestorVideosPageState(); } class _GestorVideosPageState extends State { PlutoGridStateManager? _stateManager; bool _isLoading = true; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() => _isLoading = true); final provider = Provider.of(context, listen: false); await Future.wait([ provider.loadMediaFiles(), provider.loadCategories(), ]); setState(() => _isLoading = false); } @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width <= 800; if (_isLoading) { return Center( child: CircularProgressIndicator( color: AppTheme.of(context).primaryColor, ), ); } return Consumer( builder: (context, provider, child) { if (isMobile) { return _buildMobileView(provider); } else { return _buildDesktopView(provider); } }, ); } Widget _buildDesktopView(VideosProvider provider) { return Column( children: [ _buildToolbar(provider, false), Expanded( child: Padding( padding: const EdgeInsets.all(24), child: _buildPlutoGrid(provider), ), ), ], ); } Widget _buildMobileView(VideosProvider provider) { return Column( children: [ _buildToolbar(provider, true), Expanded( child: provider.mediaFiles.isEmpty ? _buildEmptyState() : ListView.builder( padding: const EdgeInsets.all(16), itemCount: provider.mediaFiles.length, itemBuilder: (context, index) { final video = provider.mediaFiles[index]; return _buildVideoCard(video, provider); }, ), ), ], ); } Widget _buildToolbar(VideosProvider provider, bool isMobile) { return Container( padding: EdgeInsets.all(isMobile ? 16 : 24), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ AppTheme.of(context).primaryBackground, AppTheme.of(context).secondaryBackground, ], ), border: Border( bottom: BorderSide( color: AppTheme.of(context).primaryColor.withOpacity(0.1), width: 1, ), ), boxShadow: [ BoxShadow( color: AppTheme.of(context).primaryColor.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( gradient: LinearGradient( colors: [ const Color(0xFF4EC9F5), const Color(0xFFFFB733), ], ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: AppTheme.of(context).primaryColor.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: const Icon( Icons.video_library, color: Color(0xFF0B0B0D), size: 24, ), ), const Gap(16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Gestor de Videos', style: AppTheme.of(context).title2.override( fontFamily: 'Poppins', color: AppTheme.of(context).primaryText, fontWeight: FontWeight.bold, fontSize: 22, ), ), if (!isMobile) ...[ const Gap(4), Text( '${provider.mediaFiles.length} videos disponibles', style: AppTheme.of(context).bodyText2.override( fontFamily: 'Poppins', color: AppTheme.of(context).tertiaryText, fontSize: 13, ), ), ], ], ), ), PremiumButton( text: isMobile ? 'Subir' : 'Subir Video', icon: Icons.cloud_upload, onPressed: () => _showUploadDialog(provider), width: isMobile ? 100 : null, ), ], ), const Gap(16), _buildSearchField(provider), ], ), ); } Widget _buildSearchField(VideosProvider provider) { return Container( decoration: BoxDecoration( color: AppTheme.of(context).tertiaryBackground, borderRadius: BorderRadius.circular(12), border: Border.all( color: AppTheme.of(context).primaryColor.withOpacity(0.1), ), ), child: TextField( controller: provider.busquedaVideoController, style: AppTheme.of(context).bodyText1.override( fontFamily: 'Poppins', color: AppTheme.of(context).primaryText, ), decoration: InputDecoration( hintText: 'Buscar videos por título o descripción...', hintStyle: AppTheme.of(context).bodyText1.override( fontFamily: 'Poppins', color: AppTheme.of(context).tertiaryText, ), prefixIcon: Icon( Icons.search, color: AppTheme.of(context).primaryColor, ), suffixIcon: provider.busquedaVideoController.text.isNotEmpty ? IconButton( icon: Icon( Icons.clear, color: AppTheme.of(context).tertiaryText, ), onPressed: () { provider.busquedaVideoController.clear(); provider.searchVideos(''); }, ) : null, border: InputBorder.none, contentPadding: const EdgeInsets.all(16), ), onChanged: (value) => provider.searchVideos(value), ), ); } Widget _buildPlutoGrid(VideosProvider provider) { final columns = [ PlutoColumn( title: 'Vista Previa', field: 'thumbnail', type: PlutoColumnType.text(), width: 120, enableColumnDrag: false, enableSorting: false, enableContextMenu: false, renderer: (rendererContext) { final video = rendererContext.row.cells['video']?.value as MediaFileModel?; if (video == null) return const SizedBox(); return Container( margin: const EdgeInsets.all(4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: AppTheme.of(context).tertiaryBackground, ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: video.fileUrl != null ? Image.network( video.fileUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Icon( Icons.video_library, size: 32, color: AppTheme.of(context).tertiaryText, ), ) : Icon( Icons.video_library, size: 32, color: AppTheme.of(context).tertiaryText, ), ), ); }, ), PlutoColumn( title: 'Título', field: 'title', type: PlutoColumnType.text(), width: 250, ), PlutoColumn( title: 'Archivo', field: 'fileName', type: PlutoColumnType.text(), width: 200, ), PlutoColumn( title: 'Categoría', field: 'category', type: PlutoColumnType.text(), width: 150, ), PlutoColumn( title: 'Reproducciones', field: 'reproducciones', type: PlutoColumnType.number(), width: 120, textAlign: PlutoColumnTextAlign.center, ), PlutoColumn( title: 'Duración', field: 'duration', type: PlutoColumnType.text(), width: 100, ), PlutoColumn( title: 'Fecha de Creación', field: 'createdAt', type: PlutoColumnType.text(), width: 150, ), PlutoColumn( title: 'Acciones', field: 'actions', type: PlutoColumnType.text(), width: 140, enableColumnDrag: false, enableSorting: false, enableContextMenu: false, renderer: (rendererContext) { final video = rendererContext.row.cells['video']?.value as MediaFileModel?; if (video == null) return const SizedBox(); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.play_circle_outline, size: 20), color: const Color(0xFF4EC9F5), tooltip: 'Reproducir', onPressed: () => _playVideo(video), ), IconButton( icon: const Icon(Icons.edit, size: 20), color: const Color(0xFFFFB733), tooltip: 'Editar', onPressed: () => _editVideo(video, provider), ), IconButton( icon: const Icon(Icons.delete, size: 20), color: const Color(0xFFFF2D2D), tooltip: 'Eliminar', onPressed: () => _deleteVideo(video, provider), ), ], ); }, ), ]; return PlutoGrid( columns: columns, rows: provider.videosRows, onLoaded: (PlutoGridOnLoadedEvent event) { _stateManager = event.stateManager; _stateManager!.setShowColumnFilter(true); }, configuration: PlutoGridConfiguration( style: plutoGridStyleConfig(context), ), ); } Widget _buildVideoCard(MediaFileModel video, VideosProvider provider) { return Card( margin: const EdgeInsets.only(bottom: 16), color: AppTheme.of(context).secondaryBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: AppTheme.of(context).primaryColor.withOpacity(0.2), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (video.fileUrl != null) ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), child: AspectRatio( aspectRatio: 16 / 9, child: Image.network( video.fileUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container( color: AppTheme.of(context).tertiaryBackground, child: Icon( Icons.video_library, size: 64, color: AppTheme.of(context).tertiaryText, ), ), ), ), ), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( video.title ?? video.fileName, style: AppTheme.of(context).title3.override( fontFamily: 'Poppins', color: AppTheme.of(context).primaryText, fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const Gap(8), if (video.fileDescription != null && video.fileDescription!.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( video.fileDescription!, style: AppTheme.of(context).bodyText2.override( fontFamily: 'Poppins', color: AppTheme.of(context).secondaryText, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Row( children: [ Icon( Icons.play_circle_filled, size: 16, color: AppTheme.of(context).tertiaryText, ), const Gap(4), Text( '${video.reproducciones} reproducciones', style: AppTheme.of(context).bodyText2.override( fontFamily: 'Poppins', color: AppTheme.of(context).tertiaryText, fontSize: 12, ), ), if (video.durationSeconds != null) ...[ const Gap(12), Icon( Icons.access_time, size: 16, color: AppTheme.of(context).tertiaryText, ), const Gap(4), Text( _formatDuration(video.durationSeconds!), style: AppTheme.of(context).bodyText2.override( fontFamily: 'Poppins', color: AppTheme.of(context).tertiaryText, fontSize: 12, ), ), ], ], ), const Gap(12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( onPressed: () => _playVideo(video), icon: const Icon(Icons.play_circle_outline, size: 18), label: const Text('Reproducir'), style: TextButton.styleFrom( foregroundColor: const Color(0xFF4EC9F5), ), ), TextButton.icon( onPressed: () => _editVideo(video, provider), icon: const Icon(Icons.edit, size: 18), label: const Text('Editar'), style: TextButton.styleFrom( foregroundColor: const Color(0xFFFFB733), ), ), TextButton.icon( onPressed: () => _deleteVideo(video, provider), icon: const Icon(Icons.delete, size: 18), label: const Text('Eliminar'), style: TextButton.styleFrom( foregroundColor: const Color(0xFFFF2D2D), ), ), ], ), ], ), ), ], ), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.video_library_outlined, size: 80, color: AppTheme.of(context).tertiaryText, ), const Gap(16), Text( 'No hay videos disponibles', style: AppTheme.of(context).title2.override( fontFamily: 'Poppins', color: AppTheme.of(context).primaryText, fontWeight: FontWeight.bold, ), ), const Gap(8), Text( 'Sube tu primer video para comenzar', style: AppTheme.of(context).bodyText1.override( fontFamily: 'Poppins', color: AppTheme.of(context).tertiaryText, ), ), const Gap(24), ElevatedButton.icon( onPressed: () { final provider = Provider.of(context, listen: false); _showUploadDialog(provider); }, icon: const Icon(Icons.upload_file), label: const Text('Subir Video'), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.of(context).primaryColor, foregroundColor: const Color(0xFF0B0B0D), padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 16, ), ), ), ], ), ); } Future _showUploadDialog(VideosProvider provider) async { await showDialog( context: context, barrierDismissible: false, builder: (context) => PremiumUploadDialog( provider: provider, onSuccess: () { _loadData(); }, ), ); } void _playVideo(MediaFileModel video) { // TODO: Implementar reproductor de video ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Reproduciendo: ${video.title ?? video.fileName}'), ), ); } Future _editVideo(MediaFileModel video, VideosProvider provider) async { final titleController = TextEditingController(text: video.title); final descriptionController = TextEditingController(text: video.fileDescription); MediaCategoryModel? selectedCategory = provider.categories .where((cat) => cat.mediaCategoriesId == video.mediaCategoryFk) .firstOrNull; await showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) => AlertDialog( backgroundColor: AppTheme.of(context).secondaryBackground, title: Row( children: [ Icon( Icons.edit, color: AppTheme.of(context).primaryColor, ), const Gap(12), Expanded( child: Text( 'Editar Video', style: AppTheme.of(context).title2.override( fontFamily: 'Poppins', color: AppTheme.of(context).primaryText, fontWeight: FontWeight.bold, ), ), ), ], ), content: SizedBox( width: 500, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( controller: titleController, decoration: InputDecoration( labelText: 'Título', filled: true, fillColor: AppTheme.of(context).tertiaryBackground, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), ), ), const Gap(16), TextFormField( controller: descriptionController, maxLines: 3, decoration: InputDecoration( labelText: 'Descripción', filled: true, fillColor: AppTheme.of(context).tertiaryBackground, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), ), ), const Gap(16), DropdownButtonFormField( value: selectedCategory, decoration: InputDecoration( labelText: 'Categoría', filled: true, fillColor: AppTheme.of(context).tertiaryBackground, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), ), items: provider.categories.map((category) { return DropdownMenuItem( value: category, child: Text(category.categoryName), ); }).toList(), onChanged: (value) { setDialogState(() => selectedCategory = value); }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text( 'Cancelar', style: TextStyle( color: AppTheme.of(context).secondaryText, ), ), ), ElevatedButton( onPressed: () async { Navigator.pop(context); // Actualizar campos if (titleController.text != video.title) { await provider.updateVideoTitle( video.mediaFileId, titleController.text, ); } if (descriptionController.text != video.fileDescription) { await provider.updateVideoDescription( video.mediaFileId, descriptionController.text, ); } if (selectedCategory != null && selectedCategory!.mediaCategoriesId != video.mediaCategoryFk) { await provider.updateVideoCategory( video.mediaFileId, selectedCategory!.mediaCategoriesId, ); } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Video actualizado exitosamente'), backgroundColor: Colors.green, ), ); await _loadData(); }, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.of(context).primaryColor, foregroundColor: const Color(0xFF0B0B0D), ), child: const Text('Guardar'), ), ], ), ), ); } Future _deleteVideo( MediaFileModel video, VideosProvider provider) async { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: AppTheme.of(context).secondaryBackground, title: Row( children: [ const Icon( Icons.warning, color: Color(0xFFFF2D2D), ), const Gap(12), Text( 'Confirmar Eliminación', style: AppTheme.of(context).title2.override( fontFamily: 'Poppins', color: AppTheme.of(context).primaryText, fontWeight: FontWeight.bold, ), ), ], ), content: Text( '¿Estás seguro de que deseas eliminar "${video.title ?? video.fileName}"? Esta acción no se puede deshacer.', style: AppTheme.of(context).bodyText1.override( fontFamily: 'Poppins', color: AppTheme.of(context).secondaryText, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text( 'Cancelar', style: TextStyle( color: AppTheme.of(context).secondaryText, ), ), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFF2D2D), foregroundColor: Colors.white, ), child: const Text('Eliminar'), ), ], ), ); if (confirm == true) { final success = await provider.deleteVideo(video.mediaFileId); if (!mounted) return; if (success) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Video eliminado exitosamente'), backgroundColor: Colors.green, ), ); await _loadData(); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Error al eliminar el video'), backgroundColor: Color(0xFFFF2D2D), ), ); } } } 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 ${secs}s'; } else if (minutes > 0) { return '${minutes}m ${secs}s'; } else { return '${secs}s'; } } }