From 8612688aa356449632dcc9216395e613f4e55294 Mon Sep 17 00:00:00 2001 From: Abraham Date: Tue, 13 Jan 2026 14:11:03 -0800 Subject: [PATCH] =?UTF-8?q?multiples=20mejoras=20a=C3=B1adidas,=20se=20ret?= =?UTF-8?q?ir=C3=B3=20la=20categor=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/helpers/globals.dart | 2 +- lib/pages/videos/dashboard_page.dart | 256 +------------ lib/pages/videos/gestor_videos_page.dart | 269 +++++++++----- lib/pages/videos/premium_dashboard_page.dart | 343 +++++------------- lib/pages/videos/videos_layout.dart | 99 ++++- .../edit_video_dialog.dart | 56 --- .../videos/widgets/premium_upload_dialog.dart | 53 --- lib/providers/videos_provider.dart | 56 --- 8 files changed, 358 insertions(+), 776 deletions(-) diff --git a/lib/helpers/globals.dart b/lib/helpers/globals.dart index e713a2c..a6dac6e 100644 --- a/lib/helpers/globals.dart +++ b/lib/helpers/globals.dart @@ -46,7 +46,7 @@ PlutoGridScrollbarConfig plutoGridScrollbarConfig(BuildContext context) { } PlutoGridStyleConfig plutoGridStyleConfig(BuildContext context, - {double rowHeight = 100}) { + {double rowHeight = 125}) { return AppTheme.themeMode == ThemeMode.light ? PlutoGridStyleConfig( menuBackgroundColor: AppTheme.of(context).secondaryBackground, diff --git a/lib/pages/videos/dashboard_page.dart b/lib/pages/videos/dashboard_page.dart index da7325d..b5eb2b9 100644 --- a/lib/pages/videos/dashboard_page.dart +++ b/lib/pages/videos/dashboard_page.dart @@ -64,20 +64,7 @@ class _DashboardPageState extends State const Gap(24), _buildStatsCards(isMobile), const Gap(24), - if (!isMobile) ...[ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _buildCategoryChart()), - const Gap(24), - Expanded(child: _buildRecentActivity()), - ], - ), - ] else ...[ - _buildCategoryChart(), - const Gap(24), - _buildRecentActivity(), - ], + _buildRecentActivity(), ], ), ), @@ -197,13 +184,6 @@ class _DashboardPageState extends State AppTheme.of(context).secondaryColor, ), const Gap(16), - _buildStatCard( - 'Categorías', - stats['total_categories']?.toString() ?? '0', - Icons.category, - AppTheme.of(context).tertiaryColor, - ), - const Gap(16), _buildStatCard( 'Video más visto', stats['most_viewed_video']?['title'] ?? 'N/A', @@ -232,15 +212,6 @@ class _DashboardPageState extends State ), ), const Gap(16), - Expanded( - child: _buildStatCard( - 'Categorías', - stats['total_categories']?.toString() ?? '0', - Icons.category, - AppTheme.of(context).tertiaryColor, - ), - ), - const Gap(16), Expanded( child: _buildStatCard( 'Video más visto', @@ -409,231 +380,6 @@ class _DashboardPageState extends State ); } - Widget _buildCategoryChart() { - final categoriesMap = stats['videos_by_category'] as Map?; - - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - ), - borderRadius: BorderRadius.circular(24), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - blurRadius: 20, - offset: const Offset(0, 8), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: Padding( - padding: const EdgeInsets.all(28), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).primaryColor.withOpacity(0.2), - AppTheme.of(context).primaryColor.withOpacity(0.1), - ], - ), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.3), - ), - ), - child: Icon( - Icons.pie_chart_rounded, - color: AppTheme.of(context).primaryColor, - size: 24, - ), - ), - const Gap(16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Distribución por Categoría', - style: AppTheme.of(context).title3.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - const Gap(4), - Text( - 'Videos organizados por categoría', - style: AppTheme.of(context).bodyText2.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).tertiaryText, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - const Gap(24), - if (categoriesMap != null && categoriesMap.isNotEmpty) - ...categoriesMap.entries.map( - (entry) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: _buildCategoryBar( - entry.key, - entry.value, - categoriesMap.values.reduce((a, b) => a > b ? a : b), - ), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.all(40), - child: Column( - children: [ - Icon( - Icons.category_outlined, - size: 64, - color: AppTheme.of(context).tertiaryText, - ), - const Gap(16), - Text( - 'No hay datos de categorías', - style: AppTheme.of(context).bodyText1.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).tertiaryText, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildCategoryBar(String category, int count, int maxCount) { - final percentage = maxCount > 0 ? count / maxCount : 0.0; - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryBackground.withOpacity(0.5), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - category, - style: AppTheme.of(context).bodyText1.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.w600, - ), - ), - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - ], - ), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Text( - count.toString(), - style: const TextStyle( - fontFamily: 'Poppins', - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 13, - ), - ), - ), - ], - ), - const Gap(12), - Stack( - children: [ - Container( - height: 10, - decoration: BoxDecoration( - color: AppTheme.of(context).tertiaryBackground, - borderRadius: BorderRadius.circular(5), - ), - ), - FractionallySizedBox( - widthFactor: percentage, - child: Container( - height: 10, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - ], - ), - borderRadius: BorderRadius.circular(5), - boxShadow: [ - BoxShadow( - color: - AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - ), - ), - ], - ), - ], - ), - ); - } - Widget _buildRecentActivity() { return Container( decoration: BoxDecoration( diff --git a/lib/pages/videos/gestor_videos_page.dart b/lib/pages/videos/gestor_videos_page.dart index abb9cbc..00e5799 100644 --- a/lib/pages/videos/gestor_videos_page.dart +++ b/lib/pages/videos/gestor_videos_page.dart @@ -214,55 +214,43 @@ class _GestorVideosPageState extends State { ), ) else - // Vista desktop: diseño original con icono, texto y botón + // Vista desktop: contador de videos y botón (sin título redundante) 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, + child: Row( children: [ - Text( - 'Gestor de Videos', - style: AppTheme.of(context).title2.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, - fontSize: 22, - ), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFF4EC9F5).withOpacity(0.15), + const Color(0xFFFFB733).withOpacity(0.15), + ], + ), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.2), + width: 1.5, + ), + ), + child: Icon( + Icons.video_collection_rounded, + color: AppTheme.of(context).primaryColor, + size: 20, + ), ), - const Gap(4), + const Gap(12), Text( '${provider.mediaFiles.length} videos disponibles', - style: AppTheme.of(context).bodyText2.override( + style: AppTheme.of(context).bodyText1.override( fontFamily: 'Poppins', - color: AppTheme.of(context).tertiaryText, - fontSize: 13, + color: AppTheme.of(context).secondaryText, + fontSize: 15, + fontWeight: FontWeight.w500, ), ), ], @@ -334,6 +322,7 @@ class _GestorVideosPageState extends State { field: 'thumbnail', type: PlutoColumnType.text(), width: 150, + enableEditingMode: false, enableColumnDrag: false, enableSorting: false, enableContextMenu: false, @@ -411,54 +400,65 @@ class _GestorVideosPageState extends State { title: 'Título', field: 'title', type: PlutoColumnType.text(), - width: 350, + width: 300, + enableEditingMode: false, + renderer: (rendererContext) { + final title = rendererContext.cell.value?.toString() ?? ''; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + alignment: Alignment.centerLeft, + child: Text( + title, + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppTheme.of(context).primaryText, + letterSpacing: 0.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ); + }, ), PlutoColumn( title: 'Descripción', field: 'file_description', type: PlutoColumnType.text(), - width: 400, - ), - PlutoColumn( - title: 'Categoría', - field: 'category', - type: PlutoColumnType.text(), - width: 160, + width: 425, + enableEditingMode: false, 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), - ), - ], - ), + final description = rendererContext.cell.value?.toString() ?? ''; + if (description.isEmpty) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + alignment: Alignment.centerLeft, child: Text( - category.toUpperCase(), - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.w700, - letterSpacing: 0.5, + 'Sin descripción', + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 14, + fontStyle: FontStyle.italic, + color: AppTheme.of(context).tertiaryText, ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, ), + ); + } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + alignment: Alignment.centerLeft, + child: Text( + description, + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 13, + fontWeight: FontWeight.w400, + color: AppTheme.of(context).secondaryText, + height: 1.4, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, ), ); }, @@ -468,6 +468,7 @@ class _GestorVideosPageState extends State { field: 'reproducciones', type: PlutoColumnType.number(), width: 160, + enableEditingMode: false, textAlign: PlutoColumnTextAlign.center, renderer: (rendererContext) { final count = rendererContext.cell.value ?? 0; @@ -512,18 +513,72 @@ class _GestorVideosPageState extends State { field: 'duration', type: PlutoColumnType.text(), width: 180, + enableEditingMode: false, ), PlutoColumn( title: 'Fecha de Creación', field: 'createdAt', type: PlutoColumnType.text(), - width: 180, + width: 280, + enableEditingMode: false, + renderer: (rendererContext) { + final video = + rendererContext.row.cells['video']?.value as MediaFileModel?; + if (video?.createdAt == null) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + alignment: Alignment.centerLeft, + child: Text( + 'Fecha no disponible', + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 13, + fontStyle: FontStyle.italic, + color: AppTheme.of(context).tertiaryText, + ), + ), + ); + } + + final formattedDate = _formatDescriptiveDate(video!.createdAt!); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + formattedDate['date']!, + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppTheme.of(context).primaryText, + ), + ), + const SizedBox(height: 2), + Text( + formattedDate['time']!, + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 12, + fontWeight: FontWeight.w400, + color: AppTheme.of(context).secondaryText, + ), + ), + ], + ), + ); + }, ), PlutoColumn( title: 'Etiquetas', field: 'tags', type: PlutoColumnType.text(), width: 250, + enableEditingMode: false, renderer: (rendererContext) { final video = rendererContext.row.cells['video']?.value as MediaFileModel?; @@ -563,6 +618,7 @@ class _GestorVideosPageState extends State { field: 'actions', type: PlutoColumnType.text(), width: 160, + enableEditingMode: false, enableColumnDrag: false, enableSorting: false, enableContextMenu: false, @@ -605,12 +661,12 @@ class _GestorVideosPageState extends State { rows: provider.videosRows, onLoaded: (PlutoGridOnLoadedEvent event) { _stateManager = event.stateManager; - _stateManager!.setShowColumnFilter(true); + _stateManager!.setShowColumnFilter(false); }, configuration: PlutoGridConfiguration( style: plutoGridStyleConfig(context), columnSize: const PlutoGridColumnSizeConfig( - autoSizeMode: PlutoAutoSizeMode.none, + autoSizeMode: PlutoAutoSizeMode.scale, ), ), ); @@ -1174,4 +1230,51 @@ class _GestorVideosPageState extends State { return '${secs}s'; } } + + Map _formatDescriptiveDate(DateTime date) { + // Obtener día de la semana en español + final diasSemana = [ + 'lunes', + 'martes', + 'miércoles', + 'jueves', + 'viernes', + 'sábado', + 'domingo' + ]; + final diaSemana = diasSemana[date.weekday - 1]; + + // Obtener mes en español + final meses = [ + 'enero', + 'febrero', + 'marzo', + 'abril', + 'mayo', + 'junio', + 'julio', + 'agosto', + 'septiembre', + 'octubre', + 'noviembre', + 'diciembre' + ]; + final mes = meses[date.month - 1]; + + // Formatear la hora + final hora = date.hour; + final minuto = date.minute.toString().padLeft(2, '0'); + final segundo = date.second.toString().padLeft(2, '0'); + final periodo = hora >= 12 ? 'pm' : 'am'; + final hora12 = hora > 12 ? hora - 12 : (hora == 0 ? 12 : hora); + + // Construir las cadenas + final fechaTexto = '$diaSemana ${date.day} de $mes del ${date.year}'; + final horaTexto = 'a las $hora12:$minuto:$segundo $periodo'; + + return { + 'date': fechaTexto, + 'time': horaTexto, + }; + } } diff --git a/lib/pages/videos/premium_dashboard_page.dart b/lib/pages/videos/premium_dashboard_page.dart index 1cb2b8a..0ec58f2 100644 --- a/lib/pages/videos/premium_dashboard_page.dart +++ b/lib/pages/videos/premium_dashboard_page.dart @@ -79,31 +79,22 @@ class _PremiumDashboardPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildWelcomeHeader(), - const Gap(32), + /* _buildWelcomeHeader(), + const Gap(32), */ _buildStatsCards(isMobile), const Gap(32), if (!isMobile) ...[ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(flex: 3, child: _buildCategoryChart()), + Expanded(flex: 2, child: _buildViewsChart()), const Gap(24), Expanded(flex: 2, child: _buildTopVideos()), ], ), const Gap(24), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(flex: 2, child: _buildViewsChart()), - const Gap(24), - Expanded(flex: 3, child: _buildRecentActivity()), - ], - ), + _buildRecentActivity(), ] else ...[ - _buildCategoryChart(), - const Gap(24), _buildTopVideos(), const Gap(24), _buildViewsChart(), @@ -189,9 +180,8 @@ class _PremiumDashboardPageState extends State return _buildLoadingSkeleton(isMobile); } - final totalVideos = stats['totalVideos'] ?? 0; - final totalViews = stats['totalReproducciones'] ?? 0; - final categories = stats['totalCategories'] ?? 0; + final totalVideos = stats['total_videos'] ?? 0; + final totalViews = stats['total_reproducciones'] ?? 0; final avgViews = totalVideos > 0 ? (totalViews / totalVideos).round() : 0; final cards = [ @@ -215,16 +205,6 @@ class _PremiumDashboardPageState extends State trend: '+23%', trendUp: true, ), - _StatCard( - title: 'Categorías', - value: categories.toString(), - icon: Icons.category, - gradient: const LinearGradient( - colors: [Color(0xFF6B2F8A), Color(0xFF9B4FC9)], - ), - trend: '+5%', - trendUp: true, - ), _StatCard( title: 'Promedio Vistas', value: _formatNumber(avgViews), @@ -249,12 +229,12 @@ class _PremiumDashboardPageState extends State } return GridView.count( - crossAxisCount: 4, + crossAxisCount: 3, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisSpacing: 20, mainAxisSpacing: 20, - childAspectRatio: 1.5, + childAspectRatio: 2.2, children: cards, ); } @@ -264,14 +244,14 @@ class _PremiumDashboardPageState extends State baseColor: AppTheme.of(context).tertiaryBackground, highlightColor: AppTheme.of(context).secondaryBackground, child: GridView.count( - crossAxisCount: isMobile ? 1 : 4, + crossAxisCount: isMobile ? 1 : 3, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisSpacing: 20, mainAxisSpacing: 20, childAspectRatio: isMobile ? 3 : 1.5, children: List.generate( - 4, + 3, (index) => Container( decoration: BoxDecoration( color: Colors.white, @@ -283,154 +263,6 @@ class _PremiumDashboardPageState extends State ); } - Widget _buildCategoryChart() { - if (isLoading) { - return _buildChartSkeleton('Distribución por Categoría'); - } - - final videosByCategory = - stats['videosByCategory'] as Map? ?? {}; - - return Container( - padding: const EdgeInsets.all(28), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 20, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.pie_chart, - color: AppTheme.of(context).primaryColor, - size: 24, - ), - ), - const Gap(12), - Text( - 'Distribución por Categoría', - style: AppTheme.of(context).title3.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ], - ), - const Gap(32), - SizedBox( - height: 280, - child: videosByCategory.isEmpty - ? _buildEmptyChart('No hay datos de categorías') - : PieChart( - PieChartData( - sectionsSpace: 3, - centerSpaceRadius: 60, - sections: _buildPieChartSections(videosByCategory), - borderData: FlBorderData(show: false), - ), - ), - ), - const Gap(24), - _buildLegend(videosByCategory), - ], - ), - ); - } - - List _buildPieChartSections( - Map videosByCategory) { - final colors = [ - const Color(0xFF4EC9F5), - const Color(0xFFFFB733), - const Color(0xFF6B2F8A), - const Color(0xFFFF2D2D), - const Color(0xFF00C9A7), - ]; - - int index = 0; - return videosByCategory.entries.map((entry) { - final color = colors[index % colors.length]; - index++; - - return PieChartSectionData( - value: entry.value.toDouble(), - title: '${entry.value}', - color: color, - radius: 50, - titleStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color(0xFF0B0B0D), - fontFamily: 'Poppins', - ), - ); - }).toList(); - } - - Widget _buildLegend(Map videosByCategory) { - final colors = [ - const Color(0xFF4EC9F5), - const Color(0xFFFFB733), - const Color(0xFF6B2F8A), - const Color(0xFFFF2D2D), - const Color(0xFF00C9A7), - ]; - - int index = 0; - return Wrap( - spacing: 16, - runSpacing: 12, - children: videosByCategory.entries.map((entry) { - final color = colors[index % colors.length]; - index++; - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(4), - ), - ), - const Gap(8), - Text( - entry.key, - style: AppTheme.of(context).bodyText2.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).secondaryText, - fontSize: 13, - ), - ), - ], - ); - }).toList(), - ); - } - Widget _buildViewsChart() { return Container( padding: const EdgeInsets.all(28), @@ -1008,98 +840,103 @@ class _StatCardState extends State<_StatCard> onExit: (_) => setState(() => _isHovered = false), child: AnimatedContainer( duration: const Duration(milliseconds: 200), - transform: Matrix4.identity()..scale(_isHovered ? 1.03 : 1.0), + transform: Matrix4.identity()..scale(_isHovered ? 1.02 : 1.0), child: Container( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: widget.gradient, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: widget.gradient.colors.first.withOpacity(0.3), - blurRadius: _isHovered ? 20 : 12, - offset: Offset(0, _isHovered ? 8 : 4), + color: widget.gradient.colors.first.withOpacity(0.25), + blurRadius: _isHovered ? 16 : 10, + offset: Offset(0, _isHovered ? 6 : 3), ), ], ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Row( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - widget.icon, - color: const Color(0xFF0B0B0D), - size: 24, - ), - ), - if (widget.trend != null) + // Icono y valor en la izquierda + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Icono Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: widget.trendUp - ? Colors.green.withOpacity(0.2) - : Colors.red.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(10), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - widget.trendUp - ? Icons.trending_up - : Icons.trending_down, - size: 14, - color: const Color(0xFF0B0B0D), - ), - const Gap(4), - Text( - widget.trend!, - style: const TextStyle( - color: Color(0xFF0B0B0D), - fontSize: 11, - fontWeight: FontWeight.bold, - fontFamily: 'Poppins', - ), - ), - ], + child: Icon( + widget.icon, + color: const Color(0xFF0B0B0D), + size: 22, ), ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.value, - style: const TextStyle( - color: Color(0xFF0B0B0D), - fontSize: 32, - fontWeight: FontWeight.bold, - fontFamily: 'Poppins', + const Gap(12), + // Valor + Text( + widget.value, + style: const TextStyle( + color: Color(0xFF0B0B0D), + fontSize: 28, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + height: 1.0, + ), ), - ), - const Gap(4), - Text( - widget.title, - style: TextStyle( - color: const Color(0xFF0B0B0D).withOpacity(0.8), - fontSize: 14, - fontFamily: 'Poppins', - fontWeight: FontWeight.w500, + const Gap(4), + // Título + Text( + widget.title, + style: TextStyle( + color: const Color(0xFF0B0B0D).withOpacity(0.75), + fontSize: 12, + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + ), ), - ), - ], + ], + ), ), + // Trend badge a la derecha + if (widget.trend != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: widget.trendUp + ? Colors.white.withOpacity(0.3) + : Colors.black.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + widget.trendUp + ? Icons.trending_up + : Icons.trending_down, + size: 18, + color: const Color(0xFF0B0B0D), + ), + const Gap(2), + Text( + widget.trend!, + style: const TextStyle( + color: Color(0xFF0B0B0D), + fontSize: 11, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), ], ), ), diff --git a/lib/pages/videos/videos_layout.dart b/lib/pages/videos/videos_layout.dart index 7e27789..a394147 100644 --- a/lib/pages/videos/videos_layout.dart +++ b/lib/pages/videos/videos_layout.dart @@ -25,16 +25,19 @@ class _VideosLayoutState extends State { title: 'Dashboard', icon: Icons.dashboard, index: 0, + subtitle: 'Visualiza métricas y estadísticas globales de tus contenidos', ), MenuItem( title: 'Gestor de Videos', icon: Icons.video_library, index: 1, + subtitle: 'Administra, edita y organiza tu biblioteca multimedia', ), MenuItem( title: 'Configuración', icon: Icons.settings, index: 2, + subtitle: 'Personaliza las preferencias de tu plataforma', ), ]; @@ -65,6 +68,7 @@ class _VideosLayoutState extends State { Widget _buildHeader(bool isMobile) { final isDark = AppTheme.themeMode == ThemeMode.dark; final isLightBackground = !isDark; + final currentMenuItem = _menuItems[_selectedMenuIndex]; return Container( padding: EdgeInsets.all(isMobile ? 16 : 24), @@ -79,30 +83,85 @@ class _VideosLayoutState extends State { ), child: Row( children: [ - if (isMobile) + if (isMobile) ...[ IconButton( icon: const Icon(Icons.menu), color: AppTheme.of(context).primaryText, onPressed: () => _scaffoldKey.currentState?.openDrawer(), ), - // Logo de EnergyMedia - Image.asset( - isMobile - ? 'assets/images/favicon.png' - : isLightBackground - ? 'assets/images/logo_nh.png' - : 'assets/images/logo_nh_b.png', - height: isMobile ? 32 : 75, - fit: BoxFit.contain, - ), - const Spacer(), - Text( - _menuItems[_selectedMenuIndex].title, - style: AppTheme.of(context).bodyText1.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).secondaryText, + // Logo de EnergyMedia solo en mobile + Image.asset( + 'assets/images/favicon.png', + height: 32, + fit: BoxFit.contain, + ), + const Spacer(), + Text( + currentMenuItem.title, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryText, + ), + ), + ] else ...[ + // Desktop: título prominente con subtítulo + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF4EC9F5).withOpacity(0.15), + const Color(0xFFFFB733).withOpacity(0.15), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + width: 1.5, + ), + ), + child: Icon( + currentMenuItem.icon, + color: AppTheme.of(context).primaryColor, + size: 26, + ), ), - ), + const Gap(16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + currentMenuItem.title, + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 24, + letterSpacing: 0.5, + ), + ), + if (currentMenuItem.subtitle != null) ...[ + const Gap(4), + Text( + currentMenuItem.subtitle!, + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 13, + fontWeight: FontWeight.w400, + ), + ), + ], + ], + ), + ], + ), + ], ], ), ); @@ -161,7 +220,7 @@ class _VideosLayoutState extends State { ], ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ // Logo con container elegante Container( @@ -961,10 +1020,12 @@ class MenuItem { final String title; final IconData icon; final int index; + final String? subtitle; MenuItem({ required this.title, required this.icon, required this.index, + this.subtitle, }); } 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 index 7a7c619..edb624b 100644 --- a/lib/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart +++ b/lib/pages/videos/widgets/gestor_videos_widgets/edit_video_dialog.dart @@ -40,7 +40,6 @@ class _EditVideoDialogState extends State { late TextEditingController titleController; late TextEditingController descriptionController; late TextEditingController tagsController; - MediaCategoryModel? selectedCategory; Uint8List? newPosterBytes; String? newPosterFileName; VideoPlayerController? _videoPlayerController; @@ -54,9 +53,6 @@ class _EditVideoDialogState extends State { 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(); } @@ -152,15 +148,6 @@ class _EditVideoDialogState extends State { ); } - // 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]+')) @@ -346,10 +333,6 @@ class _EditVideoDialogState extends State { maxLines: 4, ), const Gap(20), - _buildLabel('Categoría'), - const Gap(8), - _buildCategoryDropdown(), - const Gap(20), _buildLabel('Etiquetas (Tags)'), const Gap(4), Text( @@ -565,45 +548,6 @@ class _EditVideoDialogState extends State { ); } - 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), diff --git a/lib/pages/videos/widgets/premium_upload_dialog.dart b/lib/pages/videos/widgets/premium_upload_dialog.dart index af70468..b30c51b 100644 --- a/lib/pages/videos/widgets/premium_upload_dialog.dart +++ b/lib/pages/videos/widgets/premium_upload_dialog.dart @@ -27,7 +27,6 @@ class _PremiumUploadDialogState extends State { final titleController = TextEditingController(); final descriptionController = TextEditingController(); final tagsController = TextEditingController(); - MediaCategoryModel? selectedCategory; Uint8List? selectedVideo; String? videoFileName; Uint8List? selectedPoster; @@ -133,7 +132,6 @@ class _PremiumUploadDialogState extends State { Future _uploadVideo() async { if (titleController.text.isEmpty || - selectedCategory == null || selectedVideo == null || videoFileName == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -165,7 +163,6 @@ class _PremiumUploadDialogState extends State { description: descriptionController.text.isEmpty ? null : descriptionController.text, - categoryId: selectedCategory!.mediaCategoriesId, tags: tags, ); @@ -359,10 +356,6 @@ class _PremiumUploadDialogState extends State { maxLines: 4, ), const Gap(20), - _buildLabel('Categoría *'), - const Gap(8), - _buildCategoryDropdown(), - const Gap(20), _buildLabel('Etiquetas (Tags)'), const Gap(4), Text( @@ -446,52 +439,6 @@ class _PremiumUploadDialogState extends State { ); } - 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), - ), - hint: Text( - 'Selecciona una categoría', - style: AppTheme.of(context).bodyText1.override( - fontFamily: 'Poppins', - color: AppTheme.of(context).tertiaryText, - ), - ), - 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 _buildVideoSelector() { return GestureDetector( onTap: _selectVideo, diff --git a/lib/providers/videos_provider.dart b/lib/providers/videos_provider.dart index 3c42991..5e97e7c 100644 --- a/lib/providers/videos_provider.dart +++ b/lib/providers/videos_provider.dart @@ -135,8 +135,6 @@ class VideosProvider extends ChangeNotifier { 'thumbnail': PlutoCell(value: media.fileUrl), 'title': PlutoCell(value: media.title ?? media.fileName), 'file_description': PlutoCell(value: media.fileDescription), - 'category': - PlutoCell(value: _getCategoryName(media.mediaCategoryFk)), 'reproducciones': PlutoCell(value: media.reproducciones), 'duration': PlutoCell( value: media.seconds != null @@ -168,18 +166,6 @@ class VideosProvider extends ChangeNotifier { } } - /// Get category name by ID - String _getCategoryName(int? categoryId) { - if (categoryId == null) return 'Sin categoría'; - try { - return categories - .firstWhere((cat) => cat.mediaCategoriesId == categoryId) - .categoryName; - } catch (e) { - return 'Sin categoría'; - } - } - /// Format file size to human readable String _formatFileSize(int? bytes) { if (bytes == null) return '-'; @@ -245,7 +231,6 @@ class VideosProvider extends ChangeNotifier { Future uploadVideo({ required String title, String? description, - int? categoryId, int? durationSeconds, List? tags, }) async { @@ -307,7 +292,6 @@ class VideosProvider extends ChangeNotifier { 'file_url': videoUrl, 'storage_path': videoStoragePath, 'organization_fk': organizationId, - 'media_category_fk': categoryId, 'metadata_json': metadataJson, 'seconds': durationSeconds, 'is_public_file': true, @@ -445,25 +429,6 @@ class VideosProvider extends ChangeNotifier { } } - /// Update video category - Future updateVideoCategory(int mediaFileId, int? categoryId) async { - try { - await supabaseML - .from('media_files') - .update({'media_category_fk': categoryId}) - .eq('media_file_id', mediaFileId) - .eq('organization_fk', organizationId); - - await loadMediaFiles(); - return true; - } catch (e) { - errorMessage = 'Error actualizando categoría: $e'; - notifyListeners(); - print('Error en updateVideoCategory: $e'); - return false; - } - } - /// Update video metadata Future updateVideoMetadata( int mediaFileId, @@ -689,29 +654,10 @@ class VideosProvider extends ChangeNotifier { curr.reproducciones > next.reproducciones ? curr : next); } - // Videos by category - Map videosByCategory = {}; - for (var media in mediaFiles) { - final categoryName = _getCategoryName(media.mediaCategoryFk); - videosByCategory[categoryName] = - (videosByCategory[categoryName] ?? 0) + 1; - } - - // Most viewed category - String? mostViewedCategory; - if (videosByCategory.isNotEmpty) { - mostViewedCategory = videosByCategory.entries - .reduce((a, b) => a.value > b.value ? a : b) - .key; - } - return { 'total_videos': totalVideos, 'total_reproducciones': totalReproducciones, 'most_viewed_video': mostViewed?.toMap(), - 'videos_by_category': videosByCategory, - 'most_viewed_category': mostViewedCategory, - 'total_categories': categories.length, }; } catch (e) { print('Error en getDashboardStats: $e'); @@ -745,8 +691,6 @@ class VideosProvider extends ChangeNotifier { 'thumbnail': PlutoCell(value: media.fileUrl), 'title': PlutoCell(value: media.title ?? media.fileName), 'file_description': PlutoCell(value: media.fileDescription), - 'category': - PlutoCell(value: _getCategoryName(media.mediaCategoryFk)), 'reproducciones': PlutoCell(value: media.reproducciones), 'duration': PlutoCell( value: media.seconds != null