diff --git a/lib/helpers/globals.dart b/lib/helpers/globals.dart index 392d1da..e713a2c 100644 --- a/lib/helpers/globals.dart +++ b/lib/helpers/globals.dart @@ -46,57 +46,80 @@ PlutoGridScrollbarConfig plutoGridScrollbarConfig(BuildContext context) { } PlutoGridStyleConfig plutoGridStyleConfig(BuildContext context, - {double rowHeight = 50}) { + {double rowHeight = 100}) { return AppTheme.themeMode == ThemeMode.light ? PlutoGridStyleConfig( - menuBackgroundColor: AppTheme.of(context).secondaryColor, - gridPopupBorderRadius: BorderRadius.circular(16), - enableColumnBorderVertical: false, - enableColumnBorderHorizontal: false, + menuBackgroundColor: AppTheme.of(context).secondaryBackground, + gridPopupBorderRadius: BorderRadius.circular(12), + enableColumnBorderVertical: true, + enableColumnBorderHorizontal: true, enableCellBorderVertical: false, enableCellBorderHorizontal: true, + columnHeight: 56, columnTextStyle: AppTheme.of(context).bodyText3.override( - fontFamily: AppTheme.of(context).bodyText3Family, + fontFamily: 'Poppins', color: AppTheme.of(context).primaryColor, + fontWeight: FontWeight.w600, + fontSize: 13, + letterSpacing: 0.5, ), - cellTextStyle: AppTheme.of(context).bodyText3, - iconColor: AppTheme.of(context).tertiaryColor, - rowColor: Colors.transparent, - borderColor: const Color(0xFFF1F4FA), + cellTextStyle: AppTheme.of(context).bodyText3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontSize: 13, + ), + iconColor: AppTheme.of(context).primaryColor, + rowColor: AppTheme.of(context).primaryBackground, + borderColor: AppTheme.of(context).hintText.withOpacity(0.5), rowHeight: rowHeight, - checkedColor: AppTheme.themeMode == ThemeMode.light - ? AppTheme.of(context).secondaryColor - : const Color(0XFF4B4B4B), + checkedColor: AppTheme.of(context).primaryColor.withOpacity(0.1), enableRowColorAnimation: true, - gridBackgroundColor: Colors.transparent, + gridBackgroundColor: AppTheme.of(context).primaryBackground, gridBorderColor: Colors.transparent, - activatedColor: AppTheme.of(context).primaryBackground, - activatedBorderColor: AppTheme.of(context).tertiaryColor, + activatedColor: AppTheme.of(context).primaryColor.withOpacity(0.05), + activatedBorderColor: + AppTheme.of(context).primaryColor.withOpacity(0.3), + columnFilterHeight: 48, + oddRowColor: + AppTheme.of(context).secondaryBackground.withOpacity(0.5), + evenRowColor: AppTheme.of(context).primaryBackground, + gridBorderRadius: BorderRadius.circular(16), ) : PlutoGridStyleConfig.dark( - menuBackgroundColor: AppTheme.of(context).secondaryColor, - gridPopupBorderRadius: BorderRadius.circular(16), - enableColumnBorderVertical: false, - enableColumnBorderHorizontal: false, + menuBackgroundColor: AppTheme.of(context).tertiaryBackground, + gridPopupBorderRadius: BorderRadius.circular(12), + enableColumnBorderVertical: true, + enableColumnBorderHorizontal: true, enableCellBorderVertical: false, enableCellBorderHorizontal: true, + columnHeight: 56, columnTextStyle: AppTheme.of(context).bodyText3.override( - fontFamily: 'Quicksand', - color: AppTheme.of(context).alternate, + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryColor, + fontWeight: FontWeight.w600, + fontSize: 13, + letterSpacing: 0.5, ), - cellTextStyle: AppTheme.of(context).bodyText3, - iconColor: AppTheme.of(context).tertiaryColor, - rowColor: Colors.transparent, - borderColor: const Color(0xFFF1F4FA), + cellTextStyle: AppTheme.of(context).bodyText3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontSize: 13, + ), + iconColor: AppTheme.of(context).primaryColor, + rowColor: AppTheme.of(context).secondaryBackground, + borderColor: AppTheme.of(context).hintText.withOpacity(0.3), rowHeight: rowHeight, - checkedColor: AppTheme.themeMode == ThemeMode.light - ? AppTheme.of(context).secondaryColor - : const Color(0XFF4B4B4B), + checkedColor: AppTheme.of(context).primaryColor.withOpacity(0.15), enableRowColorAnimation: true, - gridBackgroundColor: Colors.transparent, + gridBackgroundColor: AppTheme.of(context).secondaryBackground, gridBorderColor: Colors.transparent, - activatedColor: AppTheme.of(context).primaryBackground, - activatedBorderColor: AppTheme.of(context).tertiaryColor, + activatedColor: AppTheme.of(context).primaryColor.withOpacity(0.08), + activatedBorderColor: + AppTheme.of(context).primaryColor.withOpacity(0.4), + columnFilterHeight: 48, + oddRowColor: AppTheme.of(context).tertiaryBackground.withOpacity(0.5), + evenRowColor: AppTheme.of(context).secondaryBackground, + gridBorderRadius: BorderRadius.circular(16), ); } diff --git a/lib/pages/videos/gestor_videos_page.dart b/lib/pages/videos/gestor_videos_page.dart index 7421d1c..4fbd7e3 100644 --- a/lib/pages/videos/gestor_videos_page.dart +++ b/lib/pages/videos/gestor_videos_page.dart @@ -1,8 +1,6 @@ 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'; @@ -10,6 +8,9 @@ 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:nethive_neo/pages/videos/widgets/video_player_dialog.dart'; +import 'package:nethive_neo/pages/videos/widgets/gestor_videos_widgets/empty_state_widget.dart'; +import 'package:nethive_neo/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart'; +import 'package:nethive_neo/pages/videos/widgets/gestor_videos_widgets/delete_video_dialog.dart'; import 'package:gap/gap.dart'; class GestorVideosPage extends StatefulWidget { @@ -69,7 +70,34 @@ class _GestorVideosPageState extends State { Expanded( child: Padding( padding: const EdgeInsets.all(24), - child: _buildPlutoGrid(provider), + child: Container( + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.08), + blurRadius: 24, + offset: const Offset(0, 8), + spreadRadius: 0, + ), + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 12, + offset: const Offset(0, 4), + spreadRadius: 0, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: _buildPlutoGrid(provider), + ), + ), ), ), ], @@ -82,7 +110,9 @@ class _GestorVideosPageState extends State { _buildToolbar(provider, true), Expanded( child: provider.mediaFiles.isEmpty - ? _buildEmptyState() + ? EmptyStateWidget( + onUploadPressed: () => _showUploadDialog(provider), + ) : ListView.builder( padding: const EdgeInsets.all(16), itemCount: provider.mediaFiles.length, @@ -245,7 +275,7 @@ class _GestorVideosPageState extends State { title: 'Vista Previa', field: 'thumbnail', type: PlutoColumnType.text(), - width: 120, + width: 150, enableColumnDrag: false, enableSorting: false, enableContextMenu: false, @@ -258,28 +288,63 @@ class _GestorVideosPageState extends State { final posterUrl = video.posterUrl; return Container( - margin: const EdgeInsets.all(4), + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(10), color: AppTheme.of(context).tertiaryBackground, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: posterUrl != null && posterUrl.isNotEmpty - ? Image.network( - posterUrl, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => Icon( - Icons.video_library, - size: 32, - color: AppTheme.of(context).tertiaryText, + borderRadius: BorderRadius.circular(10), + child: Stack( + children: [ + posterUrl != null && posterUrl.isNotEmpty + ? Image.network( + posterUrl, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) => + _buildThumbnailPlaceholder(), + ) + : _buildThumbnailPlaceholder(), + // Overlay con icono de play + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.3), + ], + ), + ), + child: Center( + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + shape: BoxShape.circle, + ), + child: Icon( + Icons.play_arrow_rounded, + size: 16, + color: AppTheme.of(context).primaryColor, + ), + ), ), - ) - : Icon( - Icons.video_library, - size: 32, - color: AppTheme.of(context).tertiaryText, ), + ), + ], + ), ), ); }, @@ -288,44 +353,119 @@ class _GestorVideosPageState extends State { title: 'Título', field: 'title', type: PlutoColumnType.text(), - width: 250, + width: 350, ), PlutoColumn( title: 'Descripción', field: 'file_description', type: PlutoColumnType.text(), - width: 200, + width: 400, ), PlutoColumn( title: 'Categoría', field: 'category', type: PlutoColumnType.text(), - width: 150, + width: 160, + renderer: (rendererContext) { + final category = rendererContext.cell.value?.toString() ?? ''; + if (category.isEmpty) return const SizedBox(); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).primaryColor.withOpacity(0.7), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + category.toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ); + }, ), PlutoColumn( title: 'Reproducciones', field: 'reproducciones', type: PlutoColumnType.number(), - width: 120, + width: 160, textAlign: PlutoColumnTextAlign.center, + renderer: (rendererContext) { + final count = rendererContext.cell.value ?? 0; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: AppTheme.of(context).success.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).success.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.visibility_rounded, + size: 14, + color: AppTheme.of(context).success, + ), + const SizedBox(width: 6), + Text( + count.toString(), + style: TextStyle( + color: AppTheme.of(context).success, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + }, ), PlutoColumn( title: 'Duración', field: 'duration', type: PlutoColumnType.text(), - width: 100, + width: 180, ), PlutoColumn( title: 'Fecha de Creación', field: 'createdAt', type: PlutoColumnType.text(), - width: 150, + width: 180, ), PlutoColumn( title: 'Etiquetas', field: 'tags', type: PlutoColumnType.text(), - width: 180, + width: 250, renderer: (rendererContext) { final video = rendererContext.row.cells['video']?.value as MediaFileModel?; @@ -364,7 +504,7 @@ class _GestorVideosPageState extends State { title: 'Acciones', field: 'actions', type: PlutoColumnType.text(), - width: 140, + width: 160, enableColumnDrag: false, enableSorting: false, enableContextMenu: false, @@ -376,23 +516,25 @@ class _GestorVideosPageState extends State { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - IconButton( - icon: const Icon(Icons.play_circle_outline, size: 20), - color: const Color(0xFF4EC9F5), + _buildActionButton( + icon: Icons.play_arrow_rounded, + color: AppTheme.of(context).primaryColor, tooltip: 'Reproducir', onPressed: () => _playVideo(video), ), - IconButton( - icon: const Icon(Icons.edit, size: 20), - color: const Color(0xFFFFB733), + const SizedBox(width: 8), + _buildActionButton( + icon: Icons.edit_rounded, + color: AppTheme.of(context).secondaryColor, tooltip: 'Editar', - onPressed: () => _editVideo(video, provider), + onPressed: () => _editVideoDialog(video, provider), ), - IconButton( - icon: const Icon(Icons.delete, size: 20), - color: const Color(0xFFFF2D2D), + const SizedBox(width: 8), + _buildActionButton( + icon: Icons.delete_rounded, + color: AppTheme.of(context).error, tooltip: 'Eliminar', - onPressed: () => _deleteVideo(video, provider), + onPressed: () => _deleteVideoDialog(video, provider), ), ], ); @@ -409,6 +551,57 @@ class _GestorVideosPageState extends State { }, configuration: PlutoGridConfiguration( style: plutoGridStyleConfig(context), + columnSize: const PlutoGridColumnSizeConfig( + autoSizeMode: PlutoAutoSizeMode.none, + ), + ), + ); + } + + Widget _buildActionButton({ + required IconData icon, + required Color color, + required String tooltip, + required VoidCallback onPressed, + }) { + return Tooltip( + message: tooltip, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(10), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: color.withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: color.withOpacity(0.2), + width: 1, + ), + ), + child: Icon( + icon, + size: 18, + color: color, + ), + ), + ), + ), + ); + } + + Widget _buildThumbnailPlaceholder() { + return Container( + color: AppTheme.of(context).tertiaryBackground, + child: Center( + child: Icon( + Icons.video_library_rounded, + size: 28, + color: AppTheme.of(context).tertiaryText, + ), ), ); } @@ -535,7 +728,7 @@ class _GestorVideosPageState extends State { ), ), TextButton.icon( - onPressed: () => _editVideo(video, provider), + onPressed: () => _editVideoDialog(video, provider), icon: const Icon(Icons.edit, size: 18), label: const Text('Editar'), style: TextButton.styleFrom( @@ -543,7 +736,7 @@ class _GestorVideosPageState extends State { ), ), TextButton.icon( - onPressed: () => _deleteVideo(video, provider), + onPressed: () => _deleteVideoDialog(video, provider), icon: const Icon(Icons.delete, size: 18), label: const Text('Eliminar'), style: TextButton.styleFrom( @@ -560,56 +753,6 @@ class _GestorVideosPageState extends State { ); } - 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, @@ -653,271 +796,9 @@ class _GestorVideosPageState extends State { ); } - Future _editVideo(MediaFileModel video, VideosProvider provider) async { - 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( - 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); - }, - ), - 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, - ), - ), - ], - ], - ), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - ), - ), - ), - ElevatedButton( - onPressed: () async { - // 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, - ); - } - - // 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( - backgroundColor: AppTheme.of(context).primaryColor, - foregroundColor: const Color(0xFF0B0B0D), - ), - child: const Text('Guardar'), - ), - ], - ), - ), - ); + Future _editVideoDialog( + MediaFileModel video, VideosProvider provider) async { + final result = await EditVideoDialog.show(context, video, provider); // Manejar resultado después de cerrar el diálogo if (result == true) { @@ -934,57 +815,9 @@ class _GestorVideosPageState extends State { } } - Future _deleteVideo( + Future _deleteVideoDialog( 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'), - ), - ], - ), - ); + final confirm = await DeleteVideoDialog.show(context, video, provider); if (confirm == true) { final success = await provider.deleteVideo(video.mediaFileId); diff --git a/lib/pages/videos/videos_layout.dart b/lib/pages/videos/videos_layout.dart index 18ca214..7e27789 100644 --- a/lib/pages/videos/videos_layout.dart +++ b/lib/pages/videos/videos_layout.dart @@ -110,19 +110,33 @@ class _VideosLayoutState extends State { Widget _buildSideMenu() { return Container( - width: 280, + width: 300, decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).primaryBackground, + ], + ), border: Border( right: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), + color: AppTheme.of(context).primaryColor.withOpacity(0.15), width: 1, ), ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 30, + offset: const Offset(5, 0), + ), + ], ), child: Column( children: [ - // Header con gradiente premium + // Header con gradiente premium y efecto glass Container( width: double.infinity, padding: const EdgeInsets.all(32), @@ -131,48 +145,87 @@ class _VideosLayoutState extends State { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - const Color(0xFF4EC9F5), - const Color(0xFFFFB733), + Color(0xFF42BCEE), + Color(0xFF5865B5), + Color(0xFF653093), ], + stops: const [0.0, 0.5, 1.0], ), boxShadow: [ BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 5), + color: const Color(0xFF4EC9F5).withOpacity(0.4), + blurRadius: 30, + offset: const Offset(0, 8), + spreadRadius: 2, ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Logo de EnergyMedia - Image.asset( - 'assets/images/logo_nh_b.png', - height: 50, - fit: BoxFit.contain, - ), - const Gap(12), - Text( - 'Content Manager', - style: AppTheme.of(context).bodyText2.override( - fontFamily: 'Poppins', - color: const Color(0xFF0B0B0D).withOpacity(0.8), - fontSize: 13, + // Logo con container elegante + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), ), + ], + ), + child: Image.asset( + 'assets/images/logo_nh_b.png', + height: 40, + fit: BoxFit.contain, + ), + ), + const Gap(16), + // Título con efecto + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Content Manager', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + letterSpacing: 1.2, + ), + ), ), ], ), ), - // Menu Items + const Gap(24), + + // Sección de usuario (opcional) + if (currentUser != null) _buildUserSection(), + + const Gap(8), + + // Menu Items con mejor spacing Expanded( child: ListView( - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16), children: _menuItems.map((item) { final isSelected = _selectedMenuIndex == item.index; return Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.only(bottom: 12), child: _buildPremiumMenuItem( icon: item.icon, title: item.title, @@ -186,16 +239,24 @@ class _VideosLayoutState extends State { ), ), - // Theme Toggle en la parte inferior + // Theme Toggle mejorado Container( - padding: const EdgeInsets.all(16), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.all(6), decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - width: 1, - ), + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + width: 1, ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], ), child: Consumer( builder: (context, visualProvider, _) { @@ -204,9 +265,9 @@ class _VideosLayoutState extends State { ), ), - // Botón de Logout + // Botón de Logout premium Container( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), child: _buildLogoutButton(), ), ], @@ -214,6 +275,107 @@ class _VideosLayoutState extends State { ); } + Widget _buildUserSection() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor.withOpacity(0.1), + AppTheme.of(context).secondaryColor.withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + children: [ + // Avatar con gradiente + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xFF4EC9F5), + Color(0xFFFFB733), + ], + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: const Color(0xFF4EC9F5).withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Center( + child: Text( + (currentUser?.fullName.substring(0, 1) ?? 'U').toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + ), + ), + ), + ), + const Gap(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentUser?.fullName ?? 'Usuario', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Gap(2), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xFF4EC9F5), + Color(0xFFFFB733), + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'PREMIUM', + style: const TextStyle( + color: Colors.white, + fontSize: 9, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + letterSpacing: 0.5, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + Widget _buildPremiumMenuItem({ required IconData icon, required String title, @@ -223,25 +385,36 @@ class _VideosLayoutState extends State { return MouseRegion( cursor: SystemMouseCursors.click, child: AnimatedContainer( - duration: const Duration(milliseconds: 200), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, decoration: BoxDecoration( gradient: isSelected ? LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - const Color(0xFF4EC9F5), - const Color(0xFFFFB733), + Color(0xFF42BCEE), + Color(0xFF5865B5), + Color(0xFF653093), ], + stops: const [0.0, 0.5, 1.0], ) : null, - borderRadius: BorderRadius.circular(12), + color: isSelected ? null : Colors.transparent, + borderRadius: BorderRadius.circular(16), boxShadow: isSelected ? [ BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 12, + color: const Color(0xFF4EC9F5).withOpacity(0.5), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 0, + ), + BoxShadow( + color: const Color(0xFFFFB733).withOpacity(0.3), + blurRadius: 15, offset: const Offset(0, 4), + spreadRadius: 0, ), ] : null, @@ -250,17 +423,47 @@ class _VideosLayoutState extends State { color: Colors.transparent, child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + borderRadius: BorderRadius.circular(16), + splashColor: AppTheme.of(context).primaryColor.withOpacity(0.2), + highlightColor: AppTheme.of(context).primaryColor.withOpacity(0.1), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), child: Row( children: [ - Icon( - icon, - color: isSelected - ? const Color(0xFF0B0B0D) - : AppTheme.of(context).secondaryText, - size: 24, + // Icono con background circular + AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withOpacity(0.25) + : AppTheme.of(context).primaryColor.withOpacity(0.15), + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? Colors.white.withOpacity(0.4) + : AppTheme.of(context) + .primaryColor + .withOpacity(0.3), + width: 2, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: Colors.white.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Icon( + icon, + color: isSelected + ? Colors.white + : AppTheme.of(context).primaryColor, + size: 22, + ), ), const Gap(16), Expanded( @@ -269,22 +472,34 @@ class _VideosLayoutState extends State { style: AppTheme.of(context).bodyText1.override( fontFamily: 'Poppins', color: isSelected - ? const Color(0xFF0B0B0D) + ? Colors.white : AppTheme.of(context).primaryText, fontWeight: - isSelected ? FontWeight.bold : FontWeight.w500, + isSelected ? FontWeight.bold : FontWeight.w600, + fontSize: 15, + letterSpacing: isSelected ? 0.5 : 0, ), ), ), - if (isSelected) - Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: const Color(0xFF0B0B0D), - shape: BoxShape.circle, - ), + // Indicador animado + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: isSelected ? 8 : 0, + height: isSelected ? 8 : 0, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: isSelected + ? [ + BoxShadow( + color: Colors.white.withOpacity(0.6), + blurRadius: 8, + spreadRadius: 2, + ), + ] + : null, ), + ), ], ), ), @@ -340,24 +555,26 @@ class _VideosLayoutState extends State { return GestureDetector( onTap: onTap, child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(vertical: 12), + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 4), decoration: BoxDecoration( gradient: isSelected ? LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, colors: [ const Color(0xFF4EC9F5), const Color(0xFFFFB733), ], ) : null, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), boxShadow: isSelected ? [ BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), + color: const Color(0xFF4EC9F5).withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, 4), ), ] : null, @@ -365,23 +582,33 @@ class _VideosLayoutState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - icon, - color: isSelected - ? const Color(0xFF0B0B0D) - : AppTheme.of(context).secondaryText, - size: 20, + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withOpacity(0.2) + : Colors.transparent, + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: isSelected + ? Colors.white + : AppTheme.of(context).secondaryText, + size: 22, + ), ), - const Gap(4), + const Gap(6), Text( label, style: TextStyle( color: isSelected - ? const Color(0xFF0B0B0D) + ? Colors.white : AppTheme.of(context).secondaryText, fontSize: 11, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, fontFamily: 'Poppins', + letterSpacing: 0.5, ), ), ], @@ -537,31 +764,66 @@ class _VideosLayoutState extends State { builder: (context) => AlertDialog( backgroundColor: AppTheme.of(context).secondaryBackground, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(24), ), - title: Text( - '¿Cerrar Sesión?', - style: AppTheme.of(context).title3.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xFFFF2D2D), + Color(0xFFFF7A3D), + ], + ), + borderRadius: BorderRadius.circular(12), ), + child: const Icon( + Icons.logout_rounded, + color: Colors.white, + size: 24, + ), + ), + const Gap(16), + Expanded( + child: Text( + '¿Cerrar Sesión?', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + ), + ], ), - content: Text( - '¿Estás seguro de que deseas cerrar sesión?', - style: AppTheme.of(context).bodyText1.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).secondaryText, - ), + content: Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + '¿Estás seguro de que deseas cerrar sesión?', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryText, + ), + ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), child: Text( 'Cancelar', style: TextStyle( color: AppTheme.of(context).secondaryText, fontFamily: 'Poppins', + fontWeight: FontWeight.w600, ), ), ), @@ -573,18 +835,42 @@ class _VideosLayoutState extends State { Color(0xFFFF7A3D), ], ), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: const Color(0xFFFF2D2D).withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], ), child: TextButton( onPressed: () => Navigator.of(context).pop(true), - child: const Text( - 'Cerrar Sesión', - style: TextStyle( - color: Colors.white, - fontFamily: 'Poppins', - fontWeight: FontWeight.bold, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, ), ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.logout_rounded, + color: Colors.white, + size: 18, + ), + Gap(8), + Text( + 'Cerrar Sesión', + style: TextStyle( + color: Colors.white, + fontFamily: 'Poppins', + fontWeight: FontWeight.bold, + ), + ), + ], + ), ), ), ], @@ -602,23 +888,48 @@ class _VideosLayoutState extends State { } } }, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), decoration: BoxDecoration( - color: AppTheme.of(context).error.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).error.withOpacity(0.15), + AppTheme.of(context).error.withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(16), border: Border.all( color: AppTheme.of(context).error.withOpacity(0.3), - width: 1, + width: 2, ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).error.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], ), child: Row( children: [ - Icon( - Icons.logout, - color: AppTheme.of(context).error, - size: 24, + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppTheme.of(context).error.withOpacity(0.2), + shape: BoxShape.circle, + border: Border.all( + color: AppTheme.of(context).error.withOpacity(0.4), + width: 2, + ), + ), + child: Icon( + Icons.logout_rounded, + color: AppTheme.of(context).error, + size: 20, + ), ), const Gap(16), Expanded( @@ -627,10 +938,16 @@ class _VideosLayoutState extends State { style: AppTheme.of(context).bodyText1.override( fontFamily: 'Poppins', color: AppTheme.of(context).error, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.bold, + fontSize: 15, ), ), ), + Icon( + Icons.arrow_forward_ios_rounded, + color: AppTheme.of(context).error, + size: 16, + ), ], ), ), diff --git a/lib/pages/videos/widgets/gestor_videos_widgets/delete_video_dialog.dart b/lib/pages/videos/widgets/gestor_videos_widgets/delete_video_dialog.dart new file mode 100644 index 0000000..0c8a428 --- /dev/null +++ b/lib/pages/videos/widgets/gestor_videos_widgets/delete_video_dialog.dart @@ -0,0 +1,237 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/models/media/media_models.dart'; +import 'package:nethive_neo/providers/videos_provider.dart'; + +class DeleteVideoDialog extends StatelessWidget { + final MediaFileModel video; + final VideosProvider provider; + + const DeleteVideoDialog({ + Key? key, + required this.video, + required this.provider, + }) : super(key: key); + + static Future show( + BuildContext context, + MediaFileModel video, + VideosProvider provider, + ) { + return showDialog( + context: context, + builder: (context) => DeleteVideoDialog( + video: video, + provider: provider, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: 500, + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: AppTheme.of(context).error.withOpacity(0.3), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).error.withOpacity(0.2), + blurRadius: 30, + offset: const Offset(0, 10), + ), + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header con gradiente de error + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).error, + AppTheme.of(context).error.withOpacity(0.8), + ], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.delete_forever_rounded, + color: Colors.white, + size: 28, + ), + ), + const Gap(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Eliminar Video', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + const Gap(4), + Text( + 'Esta acción es irreversible', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: Colors.white.withOpacity(0.8), + fontSize: 13, + ), + ), + ], + ), + ), + ], + ), + ), + + // Content + Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.of(context).error.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context).error.withOpacity(0.2), + ), + ), + child: Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: AppTheme.of(context).error, + size: 24, + ), + const Gap(12), + Expanded( + child: Text( + '¿Estás seguro de que deseas eliminar "${video.title ?? video.fileName}"?', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + const Gap(16), + Text( + 'El video y todos sus datos asociados se eliminarán permanentemente. Esta acción no se puede deshacer.', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 13, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + + // Actions + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground.withOpacity(0.5), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + child: Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + ), + ), + ), + const Gap(12), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.of(context).error, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, + ), + child: const Row( + children: [ + Icon(Icons.delete_rounded, size: 18), + Gap(8), + Text( + 'Eliminar', + style: TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart b/lib/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart new file mode 100644 index 0000000..7a7c619 --- /dev/null +++ b/lib/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart @@ -0,0 +1,670 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:chewie/chewie.dart'; +import 'package:video_player/video_player.dart'; +import 'package:gap/gap.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/models/media/media_models.dart'; +import 'package:nethive_neo/providers/videos_provider.dart'; + +class EditVideoDialog extends StatefulWidget { + final MediaFileModel video; + final VideosProvider provider; + + const EditVideoDialog({ + Key? key, + required this.video, + required this.provider, + }) : super(key: key); + + static Future show( + BuildContext context, + MediaFileModel video, + VideosProvider provider, + ) { + return showDialog( + context: context, + builder: (context) => EditVideoDialog( + video: video, + provider: provider, + ), + ); + } + + @override + State createState() => _EditVideoDialogState(); +} + +class _EditVideoDialogState extends State { + late TextEditingController titleController; + late TextEditingController descriptionController; + late TextEditingController tagsController; + MediaCategoryModel? selectedCategory; + Uint8List? newPosterBytes; + String? newPosterFileName; + VideoPlayerController? _videoPlayerController; + ChewieController? _chewieController; + bool _isVideoLoading = false; + + @override + void initState() { + super.initState(); + titleController = TextEditingController(text: widget.video.title); + descriptionController = + TextEditingController(text: widget.video.fileDescription); + tagsController = TextEditingController(text: widget.video.tags.join(', ')); + selectedCategory = widget.provider.categories + .where((cat) => cat.mediaCategoriesId == widget.video.mediaCategoryFk) + .firstOrNull; + _initializeVideoPlayer(); + } + + Future _initializeVideoPlayer() async { + if (widget.video.fileUrl == null || widget.video.fileUrl!.isEmpty) return; + + setState(() => _isVideoLoading = true); + + try { + _videoPlayerController = VideoPlayerController.network( + widget.video.fileUrl!, + ); + + await _videoPlayerController!.initialize(); + + _chewieController = ChewieController( + videoPlayerController: _videoPlayerController!, + autoPlay: false, + looping: false, + showControls: true, + aspectRatio: _videoPlayerController!.value.aspectRatio, + materialProgressColors: ChewieProgressColors( + playedColor: const Color(0xFF4EC9F5), + handleColor: const Color(0xFFFFB733), + backgroundColor: Colors.grey.shade800, + bufferedColor: Colors.grey.shade600, + ), + placeholder: Container( + color: Colors.black, + child: const Center( + child: CircularProgressIndicator( + color: Color(0xFF4EC9F5), + ), + ), + ), + ); + + setState(() => _isVideoLoading = false); + } catch (e) { + setState(() => _isVideoLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error al cargar video: $e'), + backgroundColor: const Color(0xFFFF2D2D), + ), + ); + } + } + } + + @override + void dispose() { + titleController.dispose(); + descriptionController.dispose(); + tagsController.dispose(); + _chewieController?.dispose(); + _videoPlayerController?.dispose(); + super.dispose(); + } + + Future _selectPoster() 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(); + setState(() { + newPosterBytes = bytes; + newPosterFileName = image.name; + }); + } + } + + Future _saveChanges() async { + // Actualizar título + if (titleController.text != widget.video.title) { + await widget.provider.updateVideoTitle( + widget.video.mediaFileId, + titleController.text, + ); + } + + // Actualizar descripción + if (descriptionController.text != widget.video.fileDescription) { + await widget.provider.updateVideoDescription( + widget.video.mediaFileId, + descriptionController.text, + ); + } + + // Actualizar categoría + if (selectedCategory != null && + selectedCategory!.mediaCategoriesId != widget.video.mediaCategoryFk) { + await widget.provider.updateVideoCategory( + widget.video.mediaFileId, + selectedCategory!.mediaCategoriesId, + ); + } + + // Actualizar tags + final newTags = tagsController.text + .split(RegExp(r'[,\s]+')) + .map((tag) => tag.trim()) + .where((tag) => tag.isNotEmpty) + .toList(); + + if (newTags.join(',') != widget.video.tags.join(',')) { + await widget.provider.updateVideoTags( + widget.video.mediaFileId, + newTags, + ); + } + + // Actualizar portada si se seleccionó una nueva + if (newPosterBytes != null && newPosterFileName != null) { + await widget.provider.updateVideoPoster( + widget.video.mediaFileId, + newPosterBytes!, + newPosterFileName!, + ); + } + + if (!mounted) return; + Navigator.pop(context, true); + } + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width <= 800; + + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: isMobile ? double.infinity : 1000, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.9, + ), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.15), + blurRadius: 30, + offset: const Offset(0, 10), + ), + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildHeader(), + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(32), + child: + isMobile ? _buildMobileContent() : _buildDesktopContent(), + ), + ), + _buildActions(), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + ], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.edit_rounded, + color: Color(0xFF0B0B0D), + size: 28, + ), + ), + const Gap(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Editar Video', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D), + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + const Gap(4), + Text( + 'Actualiza la información y configuración', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D).withOpacity(0.7), + fontSize: 13, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => Navigator.pop(context, false), + icon: const Icon(Icons.close, color: Color(0xFF0B0B0D)), + tooltip: 'Cerrar', + ), + ], + ), + ); + } + + Widget _buildDesktopContent() { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 3, child: _buildFormFields()), + const Gap(24), + Expanded(flex: 2, child: _buildPreviewSection()), + ], + ); + } + + Widget _buildMobileContent() { + return Column( + children: [ + _buildPreviewSection(), + const Gap(24), + _buildFormFields(), + ], + ); + } + + Widget _buildFormFields() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabel('Título del Video'), + const Gap(8), + _buildTextField( + controller: titleController, + hintText: 'Título del video', + prefixIcon: Icons.title, + ), + const Gap(20), + _buildLabel('Descripción'), + const Gap(8), + _buildTextField( + controller: descriptionController, + hintText: 'Descripción del contenido', + prefixIcon: Icons.description, + maxLines: 4, + ), + const Gap(20), + _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, + ), + ], + ); + } + + Widget _buildPreviewSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabel('Vista Previa del Video'), + const Gap(12), + _buildVideoPreview(), + const Gap(16), + _buildLabel('Portada'), + const Gap(12), + _buildPosterSection(), + ], + ); + } + + Widget _buildVideoPreview() { + if (_isVideoLoading) { + return Container( + height: 250, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(16), + ), + child: const Center( + child: CircularProgressIndicator( + color: Color(0xFF4EC9F5), + ), + ), + ); + } + + if (_chewieController != null && _videoPlayerController != null) { + return Container( + height: 250, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 5), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Chewie( + controller: _chewieController!, + ), + ), + ); + } + + return Container( + height: 250, + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.video_library_rounded, + size: 64, + color: AppTheme.of(context).tertiaryText, + ), + const Gap(12), + Text( + 'Video no disponible', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + ), + ], + ), + ), + ); + } + + Widget _buildPosterSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.video.posterUrl != null) + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + widget.video.posterUrl!, + height: 140, + width: double.infinity, + fit: BoxFit.cover, + ), + ) + else + Container( + height: 140, + width: double.infinity, + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.image, + size: 48, + color: AppTheme.of(context).tertiaryText, + ), + ), + const Gap(12), + ElevatedButton.icon( + onPressed: _selectPoster, + icon: const Icon(Icons.image, size: 18), + label: Text( + newPosterBytes != null ? 'Portada Seleccionada' : 'Cambiar Portada', + ), + style: ElevatedButton.styleFrom( + backgroundColor: newPosterBytes != null + ? AppTheme.of(context).success + : AppTheme.of(context).primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, + ), + ), + if (newPosterBytes != null) ...[ + const Gap(8), + Text( + 'Nueva: $newPosterFileName', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).success, + fontSize: 11, + ), + ), + ], + ], + ); + } + + Widget _buildLabel(String text) { + return Text( + text, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String hintText, + required IconData prefixIcon, + int maxLines = 1, + }) { + 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: controller, + maxLines: maxLines, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + ), + decoration: InputDecoration( + hintText: hintText, + hintStyle: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + prefixIcon: Icon( + prefixIcon, + color: AppTheme.of(context).primaryColor, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.all(16), + ), + ), + ); + } + + Widget _buildCategoryDropdown() { + 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: DropdownButtonFormField( + value: selectedCategory, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.category, + color: AppTheme.of(context).primaryColor, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.all(16), + ), + dropdownColor: AppTheme.of(context).secondaryBackground, + items: widget.provider.categories.map((category) { + return DropdownMenuItem( + value: category, + child: Text( + category.categoryName, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + ), + ), + ); + }).toList(), + onChanged: (value) { + setState(() => selectedCategory = value); + }, + ), + ); + } + + Widget _buildActions() { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground.withOpacity(0.5), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + child: Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + ), + ), + ), + const Gap(12), + ElevatedButton( + onPressed: _saveChanges, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.of(context).primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, + ), + child: const Row( + children: [ + Icon(Icons.save_rounded, size: 18), + Gap(8), + Text( + 'Guardar', + style: TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/videos/widgets/gestor_videos_widgets/empty_state_widget.dart b/lib/pages/videos/widgets/gestor_videos_widgets/empty_state_widget.dart new file mode 100644 index 0000000..f32a84a --- /dev/null +++ b/lib/pages/videos/widgets/gestor_videos_widgets/empty_state_widget.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:gap/gap.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/providers/videos_provider.dart'; + +class EmptyStateWidget extends StatelessWidget { + final VoidCallback onUploadPressed; + + const EmptyStateWidget({ + Key? key, + required this.onUploadPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + padding: const EdgeInsets.all(48), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor.withOpacity(0.1), + AppTheme.of(context).secondaryColor.withOpacity(0.1), + ], + ), + shape: BoxShape.circle, + ), + child: Icon( + Icons.video_library_outlined, + size: 80, + color: AppTheme.of(context).primaryColor, + ), + ), + const Gap(24), + Text( + 'No hay videos disponibles', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + const Gap(12), + Text( + 'Sube tu primer video para comenzar a compartir contenido', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + textAlign: TextAlign.center, + ), + const Gap(32), + ElevatedButton.icon( + onPressed: onUploadPressed, + icon: const Icon(Icons.cloud_upload_rounded, size: 20), + label: const Text('Subir Primer Video'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.of(context).primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + shadowColor: AppTheme.of(context).primaryColor.withOpacity(0.3), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/videos/widgets/premium_upload_dialog.dart b/lib/pages/videos/widgets/premium_upload_dialog.dart index b05a3d7..af70468 100644 --- a/lib/pages/videos/widgets/premium_upload_dialog.dart +++ b/lib/pages/videos/widgets/premium_upload_dialog.dart @@ -1,6 +1,8 @@ import 'dart:typed_data'; +import 'dart:html' as html; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; +import 'package:chewie/chewie.dart'; import 'package:nethive_neo/models/media/media_models.dart'; import 'package:nethive_neo/providers/videos_provider.dart'; import 'package:nethive_neo/theme/theme.dart'; @@ -31,14 +33,22 @@ class _PremiumUploadDialogState extends State { Uint8List? selectedPoster; String? posterFileName; VideoPlayerController? _videoController; + ChewieController? _chewieController; bool isUploading = false; + bool _isVideoLoading = false; + String? _videoBlobUrl; @override void dispose() { titleController.dispose(); descriptionController.dispose(); tagsController.dispose(); + _chewieController?.dispose(); _videoController?.dispose(); + // Limpiar blob URL + if (_videoBlobUrl != null) { + html.Url.revokeObjectUrl(_videoBlobUrl!); + } super.dispose(); } @@ -51,9 +61,63 @@ class _PremiumUploadDialogState extends State { titleController.text = widget.provider.tituloController.text; }); - // Crear video player para preview (solo web) - // Para preview en web, necesitaríamos crear un Blob URL, pero esto es complejo - // Por ahora mostraremos solo el nombre y poster + // Crear video player para preview en web + await _initializeVideoPlayer(); + } + } + + Future _initializeVideoPlayer() async { + if (selectedVideo == null) return; + + setState(() => _isVideoLoading = true); + + try { + // Limpiar blob URL anterior si existe + if (_videoBlobUrl != null) { + html.Url.revokeObjectUrl(_videoBlobUrl!); + } + + // Crear Blob desde bytes + final blob = html.Blob([selectedVideo!]); + _videoBlobUrl = html.Url.createObjectUrlFromBlob(blob); + + // Inicializar video player + _videoController = VideoPlayerController.network(_videoBlobUrl!); + await _videoController!.initialize(); + + _chewieController = ChewieController( + videoPlayerController: _videoController!, + autoPlay: false, + looping: false, + showControls: true, + aspectRatio: _videoController!.value.aspectRatio, + materialProgressColors: ChewieProgressColors( + playedColor: const Color(0xFF4EC9F5), + handleColor: const Color(0xFFFFB733), + backgroundColor: Colors.grey.shade800, + bufferedColor: Colors.grey.shade600, + ), + placeholder: Container( + color: Colors.black, + child: const Center( + child: CircularProgressIndicator( + color: Color(0xFF4EC9F5), + ), + ), + ), + ); + + setState(() => _isVideoLoading = false); + } catch (e) { + setState(() => _isVideoLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error al cargar preview: $e'), + backgroundColor: const Color(0xFFFF2D2D), + ), + ); + } } } @@ -482,6 +546,80 @@ class _PremiumUploadDialogState extends State { } Widget _buildVideoPreview() { + if (_isVideoLoading) { + return Container( + color: Colors.black, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: Color(0xFF4EC9F5), + ), + Gap(12), + Text( + 'Cargando preview...', + style: TextStyle( + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ); + } + + if (_chewieController != null && _videoController != null) { + return Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(14), + child: Container( + color: Colors.black, + child: Chewie( + controller: _chewieController!, + ), + ), + ), + Positioned( + top: 12, + right: 12, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.check_circle, size: 16, color: Colors.white), + Gap(4), + Text( + 'Listo para subir', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ); + } + return Stack( children: [ ClipRRect(