Diseño mejorado

This commit is contained in:
Abraham
2026-01-12 23:27:56 -08:00
parent d1271a5578
commit 40a9be5936
7 changed files with 1849 additions and 546 deletions

View File

@@ -46,57 +46,80 @@ PlutoGridScrollbarConfig plutoGridScrollbarConfig(BuildContext context) {
} }
PlutoGridStyleConfig plutoGridStyleConfig(BuildContext context, PlutoGridStyleConfig plutoGridStyleConfig(BuildContext context,
{double rowHeight = 50}) { {double rowHeight = 100}) {
return AppTheme.themeMode == ThemeMode.light return AppTheme.themeMode == ThemeMode.light
? PlutoGridStyleConfig( ? PlutoGridStyleConfig(
menuBackgroundColor: AppTheme.of(context).secondaryColor, menuBackgroundColor: AppTheme.of(context).secondaryBackground,
gridPopupBorderRadius: BorderRadius.circular(16), gridPopupBorderRadius: BorderRadius.circular(12),
enableColumnBorderVertical: false, enableColumnBorderVertical: true,
enableColumnBorderHorizontal: false, enableColumnBorderHorizontal: true,
enableCellBorderVertical: false, enableCellBorderVertical: false,
enableCellBorderHorizontal: true, enableCellBorderHorizontal: true,
columnHeight: 56,
columnTextStyle: AppTheme.of(context).bodyText3.override( columnTextStyle: AppTheme.of(context).bodyText3.override(
fontFamily: AppTheme.of(context).bodyText3Family, fontFamily: 'Poppins',
color: AppTheme.of(context).primaryColor, color: AppTheme.of(context).primaryColor,
fontWeight: FontWeight.w600,
fontSize: 13,
letterSpacing: 0.5,
), ),
cellTextStyle: AppTheme.of(context).bodyText3, cellTextStyle: AppTheme.of(context).bodyText3.override(
iconColor: AppTheme.of(context).tertiaryColor, fontFamily: 'Poppins',
rowColor: Colors.transparent, color: AppTheme.of(context).primaryText,
borderColor: const Color(0xFFF1F4FA), fontSize: 13,
),
iconColor: AppTheme.of(context).primaryColor,
rowColor: AppTheme.of(context).primaryBackground,
borderColor: AppTheme.of(context).hintText.withOpacity(0.5),
rowHeight: rowHeight, rowHeight: rowHeight,
checkedColor: AppTheme.themeMode == ThemeMode.light checkedColor: AppTheme.of(context).primaryColor.withOpacity(0.1),
? AppTheme.of(context).secondaryColor
: const Color(0XFF4B4B4B),
enableRowColorAnimation: true, enableRowColorAnimation: true,
gridBackgroundColor: Colors.transparent, gridBackgroundColor: AppTheme.of(context).primaryBackground,
gridBorderColor: Colors.transparent, gridBorderColor: Colors.transparent,
activatedColor: AppTheme.of(context).primaryBackground, activatedColor: AppTheme.of(context).primaryColor.withOpacity(0.05),
activatedBorderColor: AppTheme.of(context).tertiaryColor, 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( : PlutoGridStyleConfig.dark(
menuBackgroundColor: AppTheme.of(context).secondaryColor, menuBackgroundColor: AppTheme.of(context).tertiaryBackground,
gridPopupBorderRadius: BorderRadius.circular(16), gridPopupBorderRadius: BorderRadius.circular(12),
enableColumnBorderVertical: false, enableColumnBorderVertical: true,
enableColumnBorderHorizontal: false, enableColumnBorderHorizontal: true,
enableCellBorderVertical: false, enableCellBorderVertical: false,
enableCellBorderHorizontal: true, enableCellBorderHorizontal: true,
columnHeight: 56,
columnTextStyle: AppTheme.of(context).bodyText3.override( columnTextStyle: AppTheme.of(context).bodyText3.override(
fontFamily: 'Quicksand', fontFamily: 'Poppins',
color: AppTheme.of(context).alternate, color: AppTheme.of(context).primaryColor,
fontWeight: FontWeight.w600,
fontSize: 13,
letterSpacing: 0.5,
), ),
cellTextStyle: AppTheme.of(context).bodyText3, cellTextStyle: AppTheme.of(context).bodyText3.override(
iconColor: AppTheme.of(context).tertiaryColor, fontFamily: 'Poppins',
rowColor: Colors.transparent, color: AppTheme.of(context).primaryText,
borderColor: const Color(0xFFF1F4FA), fontSize: 13,
),
iconColor: AppTheme.of(context).primaryColor,
rowColor: AppTheme.of(context).secondaryBackground,
borderColor: AppTheme.of(context).hintText.withOpacity(0.3),
rowHeight: rowHeight, rowHeight: rowHeight,
checkedColor: AppTheme.themeMode == ThemeMode.light checkedColor: AppTheme.of(context).primaryColor.withOpacity(0.15),
? AppTheme.of(context).secondaryColor
: const Color(0XFF4B4B4B),
enableRowColorAnimation: true, enableRowColorAnimation: true,
gridBackgroundColor: Colors.transparent, gridBackgroundColor: AppTheme.of(context).secondaryBackground,
gridBorderColor: Colors.transparent, gridBorderColor: Colors.transparent,
activatedColor: AppTheme.of(context).primaryBackground, activatedColor: AppTheme.of(context).primaryColor.withOpacity(0.08),
activatedBorderColor: AppTheme.of(context).tertiaryColor, 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),
); );
} }

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:pluto_grid/pluto_grid.dart'; import 'package:pluto_grid/pluto_grid.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart';
import 'package:nethive_neo/providers/videos_provider.dart'; import 'package:nethive_neo/providers/videos_provider.dart';
import 'package:nethive_neo/models/media/media_models.dart'; import 'package:nethive_neo/models/media/media_models.dart';
import 'package:nethive_neo/theme/theme.dart'; import 'package:nethive_neo/theme/theme.dart';
@@ -10,6 +8,9 @@ import 'package:nethive_neo/helpers/globals.dart';
import 'package:nethive_neo/widgets/premium_button.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/premium_upload_dialog.dart';
import 'package:nethive_neo/pages/videos/widgets/video_player_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'; import 'package:gap/gap.dart';
class GestorVideosPage extends StatefulWidget { class GestorVideosPage extends StatefulWidget {
@@ -69,9 +70,36 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
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), child: _buildPlutoGrid(provider),
), ),
), ),
),
),
], ],
); );
} }
@@ -82,7 +110,9 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
_buildToolbar(provider, true), _buildToolbar(provider, true),
Expanded( Expanded(
child: provider.mediaFiles.isEmpty child: provider.mediaFiles.isEmpty
? _buildEmptyState() ? EmptyStateWidget(
onUploadPressed: () => _showUploadDialog(provider),
)
: ListView.builder( : ListView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: provider.mediaFiles.length, itemCount: provider.mediaFiles.length,
@@ -245,7 +275,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
title: 'Vista Previa', title: 'Vista Previa',
field: 'thumbnail', field: 'thumbnail',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 120, width: 150,
enableColumnDrag: false, enableColumnDrag: false,
enableSorting: false, enableSorting: false,
enableContextMenu: false, enableContextMenu: false,
@@ -258,27 +288,62 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
final posterUrl = video.posterUrl; final posterUrl = video.posterUrl;
return Container( return Container(
margin: const EdgeInsets.all(4), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(10),
color: AppTheme.of(context).tertiaryBackground, color: AppTheme.of(context).tertiaryBackground,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(10),
child: posterUrl != null && posterUrl.isNotEmpty child: Stack(
children: [
posterUrl != null && posterUrl.isNotEmpty
? Image.network( ? Image.network(
posterUrl, posterUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Icon( width: double.infinity,
Icons.video_library, height: double.infinity,
size: 32, errorBuilder: (context, error, stackTrace) =>
color: AppTheme.of(context).tertiaryText, _buildThumbnailPlaceholder(),
),
) )
: Icon( : _buildThumbnailPlaceholder(),
Icons.video_library, // Overlay con icono de play
size: 32, Positioned.fill(
color: AppTheme.of(context).tertiaryText, 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,
),
),
),
),
),
],
), ),
), ),
); );
@@ -288,44 +353,119 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
title: 'Título', title: 'Título',
field: 'title', field: 'title',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 250, width: 350,
), ),
PlutoColumn( PlutoColumn(
title: 'Descripción', title: 'Descripción',
field: 'file_description', field: 'file_description',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 200, width: 400,
), ),
PlutoColumn( PlutoColumn(
title: 'Categoría', title: 'Categoría',
field: 'category', field: 'category',
type: PlutoColumnType.text(), 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( PlutoColumn(
title: 'Reproducciones', title: 'Reproducciones',
field: 'reproducciones', field: 'reproducciones',
type: PlutoColumnType.number(), type: PlutoColumnType.number(),
width: 120, width: 160,
textAlign: PlutoColumnTextAlign.center, 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( PlutoColumn(
title: 'Duración', title: 'Duración',
field: 'duration', field: 'duration',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 100, width: 180,
), ),
PlutoColumn( PlutoColumn(
title: 'Fecha de Creación', title: 'Fecha de Creación',
field: 'createdAt', field: 'createdAt',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 150, width: 180,
), ),
PlutoColumn( PlutoColumn(
title: 'Etiquetas', title: 'Etiquetas',
field: 'tags', field: 'tags',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 180, width: 250,
renderer: (rendererContext) { renderer: (rendererContext) {
final video = final video =
rendererContext.row.cells['video']?.value as MediaFileModel?; rendererContext.row.cells['video']?.value as MediaFileModel?;
@@ -364,7 +504,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
title: 'Acciones', title: 'Acciones',
field: 'actions', field: 'actions',
type: PlutoColumnType.text(), type: PlutoColumnType.text(),
width: 140, width: 160,
enableColumnDrag: false, enableColumnDrag: false,
enableSorting: false, enableSorting: false,
enableContextMenu: false, enableContextMenu: false,
@@ -376,23 +516,25 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton( _buildActionButton(
icon: const Icon(Icons.play_circle_outline, size: 20), icon: Icons.play_arrow_rounded,
color: const Color(0xFF4EC9F5), color: AppTheme.of(context).primaryColor,
tooltip: 'Reproducir', tooltip: 'Reproducir',
onPressed: () => _playVideo(video), onPressed: () => _playVideo(video),
), ),
IconButton( const SizedBox(width: 8),
icon: const Icon(Icons.edit, size: 20), _buildActionButton(
color: const Color(0xFFFFB733), icon: Icons.edit_rounded,
color: AppTheme.of(context).secondaryColor,
tooltip: 'Editar', tooltip: 'Editar',
onPressed: () => _editVideo(video, provider), onPressed: () => _editVideoDialog(video, provider),
), ),
IconButton( const SizedBox(width: 8),
icon: const Icon(Icons.delete, size: 20), _buildActionButton(
color: const Color(0xFFFF2D2D), icon: Icons.delete_rounded,
color: AppTheme.of(context).error,
tooltip: 'Eliminar', tooltip: 'Eliminar',
onPressed: () => _deleteVideo(video, provider), onPressed: () => _deleteVideoDialog(video, provider),
), ),
], ],
); );
@@ -409,6 +551,57 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
}, },
configuration: PlutoGridConfiguration( configuration: PlutoGridConfiguration(
style: plutoGridStyleConfig(context), 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<GestorVideosPage> {
), ),
), ),
TextButton.icon( TextButton.icon(
onPressed: () => _editVideo(video, provider), onPressed: () => _editVideoDialog(video, provider),
icon: const Icon(Icons.edit, size: 18), icon: const Icon(Icons.edit, size: 18),
label: const Text('Editar'), label: const Text('Editar'),
style: TextButton.styleFrom( style: TextButton.styleFrom(
@@ -543,7 +736,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
), ),
), ),
TextButton.icon( TextButton.icon(
onPressed: () => _deleteVideo(video, provider), onPressed: () => _deleteVideoDialog(video, provider),
icon: const Icon(Icons.delete, size: 18), icon: const Icon(Icons.delete, size: 18),
label: const Text('Eliminar'), label: const Text('Eliminar'),
style: TextButton.styleFrom( style: TextButton.styleFrom(
@@ -560,56 +753,6 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
); );
} }
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<VideosProvider>(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<void> _showUploadDialog(VideosProvider provider) async { Future<void> _showUploadDialog(VideosProvider provider) async {
await showDialog( await showDialog(
context: context, context: context,
@@ -653,271 +796,9 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
); );
} }
Future<void> _editVideo(MediaFileModel video, VideosProvider provider) async { Future<void> _editVideoDialog(
final titleController = TextEditingController(text: video.title); MediaFileModel video, VideosProvider provider) async {
final descriptionController = final result = await EditVideoDialog.show(context, video, provider);
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<bool>(
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<MediaCategoryModel>(
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'),
),
],
),
),
);
// Manejar resultado después de cerrar el diálogo // Manejar resultado después de cerrar el diálogo
if (result == true) { if (result == true) {
@@ -934,57 +815,9 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
} }
} }
Future<void> _deleteVideo( Future<void> _deleteVideoDialog(
MediaFileModel video, VideosProvider provider) async { MediaFileModel video, VideosProvider provider) async {
final confirm = await showDialog<bool>( final confirm = await DeleteVideoDialog.show(context, video, provider);
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.of(context).secondaryBackground,
title: Row(
children: [
const Icon(
Icons.warning,
color: Color(0xFFFF2D2D),
),
const Gap(12),
Text(
'Confirmar Eliminación',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
'¿Estás seguro de que deseas eliminar "${video.title ?? video.fileName}"? Esta acción no se puede deshacer.',
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).secondaryText,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(
'Cancelar',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
),
),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFF2D2D),
foregroundColor: Colors.white,
),
child: const Text('Eliminar'),
),
],
),
);
if (confirm == true) { if (confirm == true) {
final success = await provider.deleteVideo(video.mediaFileId); final success = await provider.deleteVideo(video.mediaFileId);

View File

@@ -110,19 +110,33 @@ class _VideosLayoutState extends State<VideosLayout> {
Widget _buildSideMenu() { Widget _buildSideMenu() {
return Container( return Container(
width: 280, width: 300,
decoration: BoxDecoration( 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( border: Border(
right: BorderSide( right: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.1), color: AppTheme.of(context).primaryColor.withOpacity(0.15),
width: 1, width: 1,
), ),
), ),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 30,
offset: const Offset(5, 0),
),
],
), ),
child: Column( child: Column(
children: [ children: [
// Header con gradiente premium // Header con gradiente premium y efecto glass
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(32),
@@ -131,48 +145,87 @@ class _VideosLayoutState extends State<VideosLayout> {
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [
const Color(0xFF4EC9F5), Color(0xFF42BCEE),
const Color(0xFFFFB733), Color(0xFF5865B5),
Color(0xFF653093),
], ],
stops: const [0.0, 0.5, 1.0],
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3), color: const Color(0xFF4EC9F5).withOpacity(0.4),
blurRadius: 20, blurRadius: 30,
offset: const Offset(0, 5), offset: const Offset(0, 8),
spreadRadius: 2,
), ),
], ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Logo de EnergyMedia // Logo con container elegante
Image.asset( 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', 'assets/images/logo_nh_b.png',
height: 50, height: 40,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
const Gap(12), ),
Text( 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', 'Content Manager',
style: AppTheme.of(context).bodyText2.override( style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins', fontFamily: 'Poppins',
color: const Color(0xFF0B0B0D).withOpacity(0.8), color: Colors.white,
fontSize: 13, 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( Expanded(
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
children: _menuItems.map((item) { children: _menuItems.map((item) {
final isSelected = _selectedMenuIndex == item.index; final isSelected = _selectedMenuIndex == item.index;
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 12),
child: _buildPremiumMenuItem( child: _buildPremiumMenuItem(
icon: item.icon, icon: item.icon,
title: item.title, title: item.title,
@@ -186,16 +239,24 @@ class _VideosLayoutState extends State<VideosLayout> {
), ),
), ),
// Theme Toggle en la parte inferior // Theme Toggle mejorado
Container( Container(
padding: const EdgeInsets.all(16), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( color: AppTheme.of(context).tertiaryBackground,
top: BorderSide( borderRadius: BorderRadius.circular(16),
color: AppTheme.of(context).primaryColor.withOpacity(0.1), border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
width: 1, width: 1,
), ),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
), ),
],
), ),
child: Consumer<VisualStateProvider>( child: Consumer<VisualStateProvider>(
builder: (context, visualProvider, _) { builder: (context, visualProvider, _) {
@@ -204,9 +265,9 @@ class _VideosLayoutState extends State<VideosLayout> {
), ),
), ),
// Botón de Logout // Botón de Logout premium
Container( Container(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
child: _buildLogoutButton(), child: _buildLogoutButton(),
), ),
], ],
@@ -214,6 +275,107 @@ class _VideosLayoutState extends State<VideosLayout> {
); );
} }
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({ Widget _buildPremiumMenuItem({
required IconData icon, required IconData icon,
required String title, required String title,
@@ -223,25 +385,36 @@ class _VideosLayoutState extends State<VideosLayout> {
return MouseRegion( return MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: isSelected gradient: isSelected
? LinearGradient( ? LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [
const Color(0xFF4EC9F5), Color(0xFF42BCEE),
const Color(0xFFFFB733), Color(0xFF5865B5),
Color(0xFF653093),
], ],
stops: const [0.0, 0.5, 1.0],
) )
: null, : null,
borderRadius: BorderRadius.circular(12), color: isSelected ? null : Colors.transparent,
borderRadius: BorderRadius.circular(16),
boxShadow: isSelected boxShadow: isSelected
? [ ? [
BoxShadow( BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3), color: const Color(0xFF4EC9F5).withOpacity(0.5),
blurRadius: 12, blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 0,
),
BoxShadow(
color: const Color(0xFFFFB733).withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 4), offset: const Offset(0, 4),
spreadRadius: 0,
), ),
] ]
: null, : null,
@@ -250,17 +423,47 @@ class _VideosLayoutState extends State<VideosLayout> {
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(16),
child: Padding( splashColor: AppTheme.of(context).primaryColor.withOpacity(0.2),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), highlightColor: AppTheme.of(context).primaryColor.withOpacity(0.1),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
child: Row( child: Row(
children: [ children: [
Icon( // 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, icon,
color: isSelected color: isSelected
? const Color(0xFF0B0B0D) ? Colors.white
: AppTheme.of(context).secondaryText, : AppTheme.of(context).primaryColor,
size: 24, size: 22,
),
), ),
const Gap(16), const Gap(16),
Expanded( Expanded(
@@ -269,20 +472,32 @@ class _VideosLayoutState extends State<VideosLayout> {
style: AppTheme.of(context).bodyText1.override( style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins', fontFamily: 'Poppins',
color: isSelected color: isSelected
? const Color(0xFF0B0B0D) ? Colors.white
: AppTheme.of(context).primaryText, : AppTheme.of(context).primaryText,
fontWeight: fontWeight:
isSelected ? FontWeight.bold : FontWeight.w500, isSelected ? FontWeight.bold : FontWeight.w600,
fontSize: 15,
letterSpacing: isSelected ? 0.5 : 0,
), ),
), ),
), ),
if (isSelected) // Indicador animado
Container( AnimatedContainer(
width: 6, duration: const Duration(milliseconds: 300),
height: 6, width: isSelected ? 8 : 0,
height: isSelected ? 8 : 0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF0B0B0D), color: Colors.white,
shape: BoxShape.circle, 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<VideosLayout> {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: isSelected gradient: isSelected
? LinearGradient( ? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [ colors: [
const Color(0xFF4EC9F5), const Color(0xFF4EC9F5),
const Color(0xFFFFB733), const Color(0xFFFFB733),
], ],
) )
: null, : null,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
boxShadow: isSelected boxShadow: isSelected
? [ ? [
BoxShadow( BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3), color: const Color(0xFF4EC9F5).withOpacity(0.4),
blurRadius: 8, blurRadius: 12,
offset: const Offset(0, 2), offset: const Offset(0, 4),
), ),
] ]
: null, : null,
@@ -365,23 +582,33 @@ class _VideosLayoutState extends State<VideosLayout> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.2)
: Colors.transparent,
shape: BoxShape.circle,
),
child: Icon(
icon, icon,
color: isSelected color: isSelected
? const Color(0xFF0B0B0D) ? Colors.white
: AppTheme.of(context).secondaryText, : AppTheme.of(context).secondaryText,
size: 20, size: 22,
), ),
const Gap(4), ),
const Gap(6),
Text( Text(
label, label,
style: TextStyle( style: TextStyle(
color: isSelected color: isSelected
? const Color(0xFF0B0B0D) ? Colors.white
: AppTheme.of(context).secondaryText, : AppTheme.of(context).secondaryText,
fontSize: 11, fontSize: 11,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
fontFamily: 'Poppins', fontFamily: 'Poppins',
letterSpacing: 0.5,
), ),
), ),
], ],
@@ -537,31 +764,66 @@ class _VideosLayoutState extends State<VideosLayout> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: AppTheme.of(context).secondaryBackground, backgroundColor: AppTheme.of(context).secondaryBackground,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(24),
), ),
title: Text( 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?', '¿Cerrar Sesión?',
style: AppTheme.of(context).title3.override( style: AppTheme.of(context).title3.override(
fontFamily: 'Poppins', fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText, color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 20,
), ),
), ),
content: Text( ),
],
),
content: Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'¿Estás seguro de que deseas cerrar sesión?', '¿Estás seguro de que deseas cerrar sesión?',
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,
), ),
), ),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
child: Text( child: Text(
'Cancelar', 'Cancelar',
style: TextStyle( style: TextStyle(
color: AppTheme.of(context).secondaryText, color: AppTheme.of(context).secondaryText,
fontFamily: 'Poppins', fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
), ),
), ),
), ),
@@ -573,11 +835,33 @@ class _VideosLayoutState extends State<VideosLayout> {
Color(0xFFFF7A3D), 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( child: TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: const Text( 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', 'Cerrar Sesión',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
@@ -585,6 +869,8 @@ class _VideosLayoutState extends State<VideosLayout> {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
],
),
), ),
), ),
], ],
@@ -602,23 +888,48 @@ class _VideosLayoutState extends State<VideosLayout> {
} }
} }
}, },
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(16),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.of(context).error.withOpacity(0.1), gradient: LinearGradient(
borderRadius: BorderRadius.circular(12), 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( border: Border.all(
color: AppTheme.of(context).error.withOpacity(0.3), 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( child: Row(
children: [ children: [
Icon( Container(
Icons.logout, 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, color: AppTheme.of(context).error,
size: 24, size: 20,
),
), ),
const Gap(16), const Gap(16),
Expanded( Expanded(
@@ -627,10 +938,16 @@ class _VideosLayoutState extends State<VideosLayout> {
style: AppTheme.of(context).bodyText1.override( style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins', fontFamily: 'Poppins',
color: AppTheme.of(context).error, 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,
),
], ],
), ),
), ),

View File

@@ -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<bool?> show(
BuildContext context,
MediaFileModel video,
VideosProvider provider,
) {
return showDialog<bool>(
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,
),
),
],
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -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<bool?> show(
BuildContext context,
MediaFileModel video,
VideosProvider provider,
) {
return showDialog<bool>(
context: context,
builder: (context) => EditVideoDialog(
video: video,
provider: provider,
),
);
}
@override
State<EditVideoDialog> createState() => _EditVideoDialogState();
}
class _EditVideoDialogState extends State<EditVideoDialog> {
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<void> _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<void> _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<void> _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<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() {
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,
),
),
],
),
),
],
),
);
}
}

View File

@@ -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),
),
),
],
),
),
);
}
}

View File

@@ -1,6 +1,8 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:html' as html;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:video_player/video_player.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/models/media/media_models.dart';
import 'package:nethive_neo/providers/videos_provider.dart'; import 'package:nethive_neo/providers/videos_provider.dart';
import 'package:nethive_neo/theme/theme.dart'; import 'package:nethive_neo/theme/theme.dart';
@@ -31,14 +33,22 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
Uint8List? selectedPoster; Uint8List? selectedPoster;
String? posterFileName; String? posterFileName;
VideoPlayerController? _videoController; VideoPlayerController? _videoController;
ChewieController? _chewieController;
bool isUploading = false; bool isUploading = false;
bool _isVideoLoading = false;
String? _videoBlobUrl;
@override @override
void dispose() { void dispose() {
titleController.dispose(); titleController.dispose();
descriptionController.dispose(); descriptionController.dispose();
tagsController.dispose(); tagsController.dispose();
_chewieController?.dispose();
_videoController?.dispose(); _videoController?.dispose();
// Limpiar blob URL
if (_videoBlobUrl != null) {
html.Url.revokeObjectUrl(_videoBlobUrl!);
}
super.dispose(); super.dispose();
} }
@@ -51,9 +61,63 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
titleController.text = widget.provider.tituloController.text; titleController.text = widget.provider.tituloController.text;
}); });
// Crear video player para preview (solo web) // Crear video player para preview en web
// Para preview en web, necesitaríamos crear un Blob URL, pero esto es complejo await _initializeVideoPlayer();
// Por ahora mostraremos solo el nombre y poster }
}
Future<void> _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<PremiumUploadDialog> {
} }
Widget _buildVideoPreview() { 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( return Stack(
children: [ children: [
ClipRRect( ClipRRect(