multiples mejoras añadidas, se retiró la categoría

This commit is contained in:
Abraham
2026-01-13 14:11:03 -08:00
parent cd23fdc071
commit 8612688aa3
8 changed files with 358 additions and 776 deletions

View File

@@ -46,7 +46,7 @@ PlutoGridScrollbarConfig plutoGridScrollbarConfig(BuildContext context) {
} }
PlutoGridStyleConfig plutoGridStyleConfig(BuildContext context, PlutoGridStyleConfig plutoGridStyleConfig(BuildContext context,
{double rowHeight = 100}) { {double rowHeight = 125}) {
return AppTheme.themeMode == ThemeMode.light return AppTheme.themeMode == ThemeMode.light
? PlutoGridStyleConfig( ? PlutoGridStyleConfig(
menuBackgroundColor: AppTheme.of(context).secondaryBackground, menuBackgroundColor: AppTheme.of(context).secondaryBackground,

View File

@@ -64,21 +64,8 @@ class _DashboardPageState extends State<DashboardPage>
const Gap(24), const Gap(24),
_buildStatsCards(isMobile), _buildStatsCards(isMobile),
const Gap(24), 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<DashboardPage>
AppTheme.of(context).secondaryColor, AppTheme.of(context).secondaryColor,
), ),
const Gap(16), const Gap(16),
_buildStatCard(
'Categorías',
stats['total_categories']?.toString() ?? '0',
Icons.category,
AppTheme.of(context).tertiaryColor,
),
const Gap(16),
_buildStatCard( _buildStatCard(
'Video más visto', 'Video más visto',
stats['most_viewed_video']?['title'] ?? 'N/A', stats['most_viewed_video']?['title'] ?? 'N/A',
@@ -232,15 +212,6 @@ class _DashboardPageState extends State<DashboardPage>
), ),
), ),
const Gap(16), const Gap(16),
Expanded(
child: _buildStatCard(
'Categorías',
stats['total_categories']?.toString() ?? '0',
Icons.category,
AppTheme.of(context).tertiaryColor,
),
),
const Gap(16),
Expanded( Expanded(
child: _buildStatCard( child: _buildStatCard(
'Video más visto', 'Video más visto',
@@ -409,231 +380,6 @@ class _DashboardPageState extends State<DashboardPage>
); );
} }
Widget _buildCategoryChart() {
final categoriesMap = stats['videos_by_category'] as Map<String, dynamic>?;
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() { Widget _buildRecentActivity() {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@@ -214,55 +214,43 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
), ),
) )
else 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( Row(
children: [
Expanded(
child: Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
const Color(0xFF4EC9F5), const Color(0xFF4EC9F5).withOpacity(0.15),
const Color(0xFFFFB733), const Color(0xFFFFB733).withOpacity(0.15),
], ],
), ),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(10),
boxShadow: [ border: Border.all(
BoxShadow( color: AppTheme.of(context)
color: .primaryColor
AppTheme.of(context).primaryColor.withOpacity(0.3), .withOpacity(0.2),
blurRadius: 8, width: 1.5,
offset: const Offset(0, 2),
),
],
),
child: const Icon(
Icons.video_library,
color: Color(0xFF0B0B0D),
size: 24,
), ),
), ),
const Gap(16), child: Icon(
Expanded( Icons.video_collection_rounded,
child: Column( color: AppTheme.of(context).primaryColor,
crossAxisAlignment: CrossAxisAlignment.start, size: 20,
children: [
Text(
'Gestor de Videos',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
fontSize: 22,
), ),
), ),
const Gap(4), const Gap(12),
Text( Text(
'${provider.mediaFiles.length} videos disponibles', '${provider.mediaFiles.length} videos disponibles',
style: AppTheme.of(context).bodyText2.override( style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins', fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText, color: AppTheme.of(context).secondaryText,
fontSize: 13, fontSize: 15,
fontWeight: FontWeight.w500,
), ),
), ),
], ],
@@ -334,6 +322,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
field: 'thumbnail', field: 'thumbnail',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 150, width: 150,
enableEditingMode: false,
enableColumnDrag: false, enableColumnDrag: false,
enableSorting: false, enableSorting: false,
enableContextMenu: false, enableContextMenu: false,
@@ -411,55 +400,66 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
title: 'Título', title: 'Título',
field: 'title', field: 'title',
type: PlutoColumnType.text(), 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( PlutoColumn(
title: 'Descripción', title: 'Descripción',
field: 'file_description', field: 'file_description',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 400, width: 425,
), enableEditingMode: false,
PlutoColumn(
title: 'Categoría',
field: 'category',
type: PlutoColumnType.text(),
width: 160,
renderer: (rendererContext) { renderer: (rendererContext) {
final category = rendererContext.cell.value?.toString() ?? ''; final description = rendererContext.cell.value?.toString() ?? '';
if (category.isEmpty) return const SizedBox(); if (description.isEmpty) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Container( alignment: Alignment.centerLeft,
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( child: Text(
category.toUpperCase(), 'Sin descripción',
style: const TextStyle( style: TextStyle(
color: Colors.white, fontFamily: 'Poppins',
fontSize: 10, fontSize: 14,
fontWeight: FontWeight.w700, fontStyle: FontStyle.italic,
letterSpacing: 0.5, color: AppTheme.of(context).tertiaryText,
), ),
textAlign: TextAlign.center, ),
);
}
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, overflow: TextOverflow.ellipsis,
), ),
),
); );
}, },
), ),
@@ -468,6 +468,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
field: 'reproducciones', field: 'reproducciones',
type: PlutoColumnType.number(), type: PlutoColumnType.number(),
width: 160, width: 160,
enableEditingMode: false,
textAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.center,
renderer: (rendererContext) { renderer: (rendererContext) {
final count = rendererContext.cell.value ?? 0; final count = rendererContext.cell.value ?? 0;
@@ -512,18 +513,72 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
field: 'duration', field: 'duration',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 180, width: 180,
enableEditingMode: false,
), ),
PlutoColumn( PlutoColumn(
title: 'Fecha de Creación', title: 'Fecha de Creación',
field: 'createdAt', field: 'createdAt',
type: PlutoColumnType.text(), 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( PlutoColumn(
title: 'Etiquetas', title: 'Etiquetas',
field: 'tags', field: 'tags',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 250, width: 250,
enableEditingMode: false,
renderer: (rendererContext) { renderer: (rendererContext) {
final video = final video =
rendererContext.row.cells['video']?.value as MediaFileModel?; rendererContext.row.cells['video']?.value as MediaFileModel?;
@@ -563,6 +618,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
field: 'actions', field: 'actions',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 160, width: 160,
enableEditingMode: false,
enableColumnDrag: false, enableColumnDrag: false,
enableSorting: false, enableSorting: false,
enableContextMenu: false, enableContextMenu: false,
@@ -605,12 +661,12 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
rows: provider.videosRows, rows: provider.videosRows,
onLoaded: (PlutoGridOnLoadedEvent event) { onLoaded: (PlutoGridOnLoadedEvent event) {
_stateManager = event.stateManager; _stateManager = event.stateManager;
_stateManager!.setShowColumnFilter(true); _stateManager!.setShowColumnFilter(false);
}, },
configuration: PlutoGridConfiguration( configuration: PlutoGridConfiguration(
style: plutoGridStyleConfig(context), style: plutoGridStyleConfig(context),
columnSize: const PlutoGridColumnSizeConfig( columnSize: const PlutoGridColumnSizeConfig(
autoSizeMode: PlutoAutoSizeMode.none, autoSizeMode: PlutoAutoSizeMode.scale,
), ),
), ),
); );
@@ -1174,4 +1230,51 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
return '${secs}s'; return '${secs}s';
} }
} }
Map<String, String> _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,
};
}
} }

View File

@@ -79,31 +79,22 @@ class _PremiumDashboardPageState extends State<PremiumDashboardPage>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildWelcomeHeader(), /* _buildWelcomeHeader(),
const Gap(32), const Gap(32), */
_buildStatsCards(isMobile), _buildStatsCards(isMobile),
const Gap(32), const Gap(32),
if (!isMobile) ...[ if (!isMobile) ...[
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded(flex: 3, child: _buildCategoryChart()), Expanded(flex: 2, child: _buildViewsChart()),
const Gap(24), const Gap(24),
Expanded(flex: 2, child: _buildTopVideos()), Expanded(flex: 2, child: _buildTopVideos()),
], ],
), ),
const Gap(24), const Gap(24),
Row( _buildRecentActivity(),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 2, child: _buildViewsChart()),
const Gap(24),
Expanded(flex: 3, child: _buildRecentActivity()),
],
),
] else ...[ ] else ...[
_buildCategoryChart(),
const Gap(24),
_buildTopVideos(), _buildTopVideos(),
const Gap(24), const Gap(24),
_buildViewsChart(), _buildViewsChart(),
@@ -189,9 +180,8 @@ class _PremiumDashboardPageState extends State<PremiumDashboardPage>
return _buildLoadingSkeleton(isMobile); return _buildLoadingSkeleton(isMobile);
} }
final totalVideos = stats['totalVideos'] ?? 0; final totalVideos = stats['total_videos'] ?? 0;
final totalViews = stats['totalReproducciones'] ?? 0; final totalViews = stats['total_reproducciones'] ?? 0;
final categories = stats['totalCategories'] ?? 0;
final avgViews = totalVideos > 0 ? (totalViews / totalVideos).round() : 0; final avgViews = totalVideos > 0 ? (totalViews / totalVideos).round() : 0;
final cards = [ final cards = [
@@ -215,16 +205,6 @@ class _PremiumDashboardPageState extends State<PremiumDashboardPage>
trend: '+23%', trend: '+23%',
trendUp: true, 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( _StatCard(
title: 'Promedio Vistas', title: 'Promedio Vistas',
value: _formatNumber(avgViews), value: _formatNumber(avgViews),
@@ -249,12 +229,12 @@ class _PremiumDashboardPageState extends State<PremiumDashboardPage>
} }
return GridView.count( return GridView.count(
crossAxisCount: 4, crossAxisCount: 3,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 20, crossAxisSpacing: 20,
mainAxisSpacing: 20, mainAxisSpacing: 20,
childAspectRatio: 1.5, childAspectRatio: 2.2,
children: cards, children: cards,
); );
} }
@@ -264,14 +244,14 @@ class _PremiumDashboardPageState extends State<PremiumDashboardPage>
baseColor: AppTheme.of(context).tertiaryBackground, baseColor: AppTheme.of(context).tertiaryBackground,
highlightColor: AppTheme.of(context).secondaryBackground, highlightColor: AppTheme.of(context).secondaryBackground,
child: GridView.count( child: GridView.count(
crossAxisCount: isMobile ? 1 : 4, crossAxisCount: isMobile ? 1 : 3,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 20, crossAxisSpacing: 20,
mainAxisSpacing: 20, mainAxisSpacing: 20,
childAspectRatio: isMobile ? 3 : 1.5, childAspectRatio: isMobile ? 3 : 1.5,
children: List.generate( children: List.generate(
4, 3,
(index) => Container( (index) => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@@ -283,154 +263,6 @@ class _PremiumDashboardPageState extends State<PremiumDashboardPage>
); );
} }
Widget _buildCategoryChart() {
if (isLoading) {
return _buildChartSkeleton('Distribución por Categoría');
}
final videosByCategory =
stats['videosByCategory'] as Map<String, int>? ?? {};
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<PieChartSectionData> _buildPieChartSections(
Map<String, int> 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<String, int> 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() { Widget _buildViewsChart() {
return Container( return Container(
padding: const EdgeInsets.all(28), padding: const EdgeInsets.all(28),
@@ -1008,60 +840,91 @@ class _StatCardState extends State<_StatCard>
onExit: (_) => setState(() => _isHovered = false), onExit: (_) => setState(() => _isHovered = false),
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 200), 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( child: Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: widget.gradient, gradient: widget.gradient,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: widget.gradient.colors.first.withOpacity(0.3), color: widget.gradient.colors.first.withOpacity(0.25),
blurRadius: _isHovered ? 20 : 12, blurRadius: _isHovered ? 16 : 10,
offset: Offset(0, _isHovered ? 8 : 4), offset: Offset(0, _isHovered ? 6 : 3),
), ),
], ],
), ),
child: Row(
children: [
// Icono y valor en la izquierda
Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// Icono
Container( Container(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.25),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Icon( child: Icon(
widget.icon, widget.icon,
color: const Color(0xFF0B0B0D), color: const Color(0xFF0B0B0D),
size: 24, size: 22,
), ),
), ),
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),
// 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) if (widget.trend != null)
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4), horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: widget.trendUp color: widget.trendUp
? Colors.green.withOpacity(0.2) ? Colors.white.withOpacity(0.3)
: Colors.red.withOpacity(0.2), : Colors.black.withOpacity(0.2),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(10),
), ),
child: Row( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(
widget.trendUp widget.trendUp
? Icons.trending_up ? Icons.trending_up
: Icons.trending_down, : Icons.trending_down,
size: 14, size: 18,
color: const Color(0xFF0B0B0D), color: const Color(0xFF0B0B0D),
), ),
const Gap(4), const Gap(2),
Text( Text(
widget.trend!, widget.trend!,
style: const TextStyle( style: const TextStyle(
@@ -1076,32 +939,6 @@ class _StatCardState extends State<_StatCard>
), ),
], ],
), ),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.value,
style: const TextStyle(
color: Color(0xFF0B0B0D),
fontSize: 32,
fontWeight: FontWeight.bold,
fontFamily: 'Poppins',
),
),
const Gap(4),
Text(
widget.title,
style: TextStyle(
color: const Color(0xFF0B0B0D).withOpacity(0.8),
fontSize: 14,
fontFamily: 'Poppins',
fontWeight: FontWeight.w500,
),
),
],
),
],
),
), ),
), ),
); );

View File

@@ -25,16 +25,19 @@ class _VideosLayoutState extends State<VideosLayout> {
title: 'Dashboard', title: 'Dashboard',
icon: Icons.dashboard, icon: Icons.dashboard,
index: 0, index: 0,
subtitle: 'Visualiza métricas y estadísticas globales de tus contenidos',
), ),
MenuItem( MenuItem(
title: 'Gestor de Videos', title: 'Gestor de Videos',
icon: Icons.video_library, icon: Icons.video_library,
index: 1, index: 1,
subtitle: 'Administra, edita y organiza tu biblioteca multimedia',
), ),
MenuItem( MenuItem(
title: 'Configuración', title: 'Configuración',
icon: Icons.settings, icon: Icons.settings,
index: 2, index: 2,
subtitle: 'Personaliza las preferencias de tu plataforma',
), ),
]; ];
@@ -65,6 +68,7 @@ class _VideosLayoutState extends State<VideosLayout> {
Widget _buildHeader(bool isMobile) { Widget _buildHeader(bool isMobile) {
final isDark = AppTheme.themeMode == ThemeMode.dark; final isDark = AppTheme.themeMode == ThemeMode.dark;
final isLightBackground = !isDark; final isLightBackground = !isDark;
final currentMenuItem = _menuItems[_selectedMenuIndex];
return Container( return Container(
padding: EdgeInsets.all(isMobile ? 16 : 24), padding: EdgeInsets.all(isMobile ? 16 : 24),
@@ -79,30 +83,85 @@ class _VideosLayoutState extends State<VideosLayout> {
), ),
child: Row( child: Row(
children: [ children: [
if (isMobile) if (isMobile) ...[
IconButton( IconButton(
icon: const Icon(Icons.menu), icon: const Icon(Icons.menu),
color: AppTheme.of(context).primaryText, color: AppTheme.of(context).primaryText,
onPressed: () => _scaffoldKey.currentState?.openDrawer(), onPressed: () => _scaffoldKey.currentState?.openDrawer(),
), ),
// Logo de EnergyMedia // Logo de EnergyMedia solo en mobile
Image.asset( Image.asset(
isMobile 'assets/images/favicon.png',
? 'assets/images/favicon.png' height: 32,
: isLightBackground
? 'assets/images/logo_nh.png'
: 'assets/images/logo_nh_b.png',
height: isMobile ? 32 : 75,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
const Spacer(), const Spacer(),
Text( Text(
_menuItems[_selectedMenuIndex].title, currentMenuItem.title,
style: AppTheme.of(context).bodyText1.override( style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins', fontFamily: 'Poppins',
color: AppTheme.of(context).secondaryText, 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<VideosLayout> {
], ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Logo con container elegante // Logo con container elegante
Container( Container(
@@ -961,10 +1020,12 @@ class MenuItem {
final String title; final String title;
final IconData icon; final IconData icon;
final int index; final int index;
final String? subtitle;
MenuItem({ MenuItem({
required this.title, required this.title,
required this.icon, required this.icon,
required this.index, required this.index,
this.subtitle,
}); });
} }

View File

@@ -40,7 +40,6 @@ class _EditVideoDialogState extends State<EditVideoDialog> {
late TextEditingController titleController; late TextEditingController titleController;
late TextEditingController descriptionController; late TextEditingController descriptionController;
late TextEditingController tagsController; late TextEditingController tagsController;
MediaCategoryModel? selectedCategory;
Uint8List? newPosterBytes; Uint8List? newPosterBytes;
String? newPosterFileName; String? newPosterFileName;
VideoPlayerController? _videoPlayerController; VideoPlayerController? _videoPlayerController;
@@ -54,9 +53,6 @@ class _EditVideoDialogState extends State<EditVideoDialog> {
descriptionController = descriptionController =
TextEditingController(text: widget.video.fileDescription); TextEditingController(text: widget.video.fileDescription);
tagsController = TextEditingController(text: widget.video.tags.join(', ')); tagsController = TextEditingController(text: widget.video.tags.join(', '));
selectedCategory = widget.provider.categories
.where((cat) => cat.mediaCategoriesId == widget.video.mediaCategoryFk)
.firstOrNull;
_initializeVideoPlayer(); _initializeVideoPlayer();
} }
@@ -152,15 +148,6 @@ class _EditVideoDialogState extends State<EditVideoDialog> {
); );
} }
// Actualizar categoría
if (selectedCategory != null &&
selectedCategory!.mediaCategoriesId != widget.video.mediaCategoryFk) {
await widget.provider.updateVideoCategory(
widget.video.mediaFileId,
selectedCategory!.mediaCategoriesId,
);
}
// Actualizar tags // Actualizar tags
final newTags = tagsController.text final newTags = tagsController.text
.split(RegExp(r'[,\s]+')) .split(RegExp(r'[,\s]+'))
@@ -346,10 +333,6 @@ class _EditVideoDialogState extends State<EditVideoDialog> {
maxLines: 4, maxLines: 4,
), ),
const Gap(20), const Gap(20),
_buildLabel('Categoría'),
const Gap(8),
_buildCategoryDropdown(),
const Gap(20),
_buildLabel('Etiquetas (Tags)'), _buildLabel('Etiquetas (Tags)'),
const Gap(4), const Gap(4),
Text( Text(
@@ -565,45 +548,6 @@ class _EditVideoDialogState extends State<EditVideoDialog> {
); );
} }
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<MediaCategoryModel>(
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() { Widget _buildActions() {
return Container( return Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),

View File

@@ -27,7 +27,6 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
final titleController = TextEditingController(); final titleController = TextEditingController();
final descriptionController = TextEditingController(); final descriptionController = TextEditingController();
final tagsController = TextEditingController(); final tagsController = TextEditingController();
MediaCategoryModel? selectedCategory;
Uint8List? selectedVideo; Uint8List? selectedVideo;
String? videoFileName; String? videoFileName;
Uint8List? selectedPoster; Uint8List? selectedPoster;
@@ -133,7 +132,6 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
Future<void> _uploadVideo() async { Future<void> _uploadVideo() async {
if (titleController.text.isEmpty || if (titleController.text.isEmpty ||
selectedCategory == null ||
selectedVideo == null || selectedVideo == null ||
videoFileName == null) { videoFileName == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -165,7 +163,6 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
description: descriptionController.text.isEmpty description: descriptionController.text.isEmpty
? null ? null
: descriptionController.text, : descriptionController.text,
categoryId: selectedCategory!.mediaCategoriesId,
tags: tags, tags: tags,
); );
@@ -359,10 +356,6 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
maxLines: 4, maxLines: 4,
), ),
const Gap(20), const Gap(20),
_buildLabel('Categoría *'),
const Gap(8),
_buildCategoryDropdown(),
const Gap(20),
_buildLabel('Etiquetas (Tags)'), _buildLabel('Etiquetas (Tags)'),
const Gap(4), const Gap(4),
Text( Text(
@@ -446,52 +439,6 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
); );
} }
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<MediaCategoryModel>(
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() { Widget _buildVideoSelector() {
return GestureDetector( return GestureDetector(
onTap: _selectVideo, onTap: _selectVideo,

View File

@@ -135,8 +135,6 @@ class VideosProvider extends ChangeNotifier {
'thumbnail': PlutoCell(value: media.fileUrl), 'thumbnail': PlutoCell(value: media.fileUrl),
'title': PlutoCell(value: media.title ?? media.fileName), 'title': PlutoCell(value: media.title ?? media.fileName),
'file_description': PlutoCell(value: media.fileDescription), 'file_description': PlutoCell(value: media.fileDescription),
'category':
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
'reproducciones': PlutoCell(value: media.reproducciones), 'reproducciones': PlutoCell(value: media.reproducciones),
'duration': PlutoCell( 'duration': PlutoCell(
value: media.seconds != null 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 /// Format file size to human readable
String _formatFileSize(int? bytes) { String _formatFileSize(int? bytes) {
if (bytes == null) return '-'; if (bytes == null) return '-';
@@ -245,7 +231,6 @@ class VideosProvider extends ChangeNotifier {
Future<bool> uploadVideo({ Future<bool> uploadVideo({
required String title, required String title,
String? description, String? description,
int? categoryId,
int? durationSeconds, int? durationSeconds,
List<String>? tags, List<String>? tags,
}) async { }) async {
@@ -307,7 +292,6 @@ class VideosProvider extends ChangeNotifier {
'file_url': videoUrl, 'file_url': videoUrl,
'storage_path': videoStoragePath, 'storage_path': videoStoragePath,
'organization_fk': organizationId, 'organization_fk': organizationId,
'media_category_fk': categoryId,
'metadata_json': metadataJson, 'metadata_json': metadataJson,
'seconds': durationSeconds, 'seconds': durationSeconds,
'is_public_file': true, 'is_public_file': true,
@@ -445,25 +429,6 @@ class VideosProvider extends ChangeNotifier {
} }
} }
/// Update video category
Future<bool> 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 /// Update video metadata
Future<bool> updateVideoMetadata( Future<bool> updateVideoMetadata(
int mediaFileId, int mediaFileId,
@@ -689,29 +654,10 @@ class VideosProvider extends ChangeNotifier {
curr.reproducciones > next.reproducciones ? curr : next); curr.reproducciones > next.reproducciones ? curr : next);
} }
// Videos by category
Map<String, int> 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 { return {
'total_videos': totalVideos, 'total_videos': totalVideos,
'total_reproducciones': totalReproducciones, 'total_reproducciones': totalReproducciones,
'most_viewed_video': mostViewed?.toMap(), 'most_viewed_video': mostViewed?.toMap(),
'videos_by_category': videosByCategory,
'most_viewed_category': mostViewedCategory,
'total_categories': categories.length,
}; };
} catch (e) { } catch (e) {
print('Error en getDashboardStats: $e'); print('Error en getDashboardStats: $e');
@@ -745,8 +691,6 @@ class VideosProvider extends ChangeNotifier {
'thumbnail': PlutoCell(value: media.fileUrl), 'thumbnail': PlutoCell(value: media.fileUrl),
'title': PlutoCell(value: media.title ?? media.fileName), 'title': PlutoCell(value: media.title ?? media.fileName),
'file_description': PlutoCell(value: media.fileDescription), 'file_description': PlutoCell(value: media.fileDescription),
'category':
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
'reproducciones': PlutoCell(value: media.reproducciones), 'reproducciones': PlutoCell(value: media.reproducciones),
'duration': PlutoCell( 'duration': PlutoCell(
value: media.seconds != null value: media.seconds != null