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

View File

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

View File

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

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:html' as html;
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
import 'package:nethive_neo/models/media/media_models.dart';
import 'package:nethive_neo/providers/videos_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
@@ -31,14 +33,22 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
Uint8List? selectedPoster;
String? posterFileName;
VideoPlayerController? _videoController;
ChewieController? _chewieController;
bool isUploading = false;
bool _isVideoLoading = false;
String? _videoBlobUrl;
@override
void dispose() {
titleController.dispose();
descriptionController.dispose();
tagsController.dispose();
_chewieController?.dispose();
_videoController?.dispose();
// Limpiar blob URL
if (_videoBlobUrl != null) {
html.Url.revokeObjectUrl(_videoBlobUrl!);
}
super.dispose();
}
@@ -51,9 +61,63 @@ class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
titleController.text = widget.provider.tituloController.text;
});
// Crear video player para preview (solo web)
// Para preview en web, necesitaríamos crear un Blob URL, pero esto es complejo
// Por ahora mostraremos solo el nombre y poster
// Crear video player para preview en web
await _initializeVideoPlayer();
}
}
Future<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() {
if (_isVideoLoading) {
return Container(
color: Colors.black,
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Color(0xFF4EC9F5),
),
Gap(12),
Text(
'Cargando preview...',
style: TextStyle(
color: Colors.white,
fontFamily: 'Poppins',
),
),
],
),
),
);
}
if (_chewieController != null && _videoController != null) {
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(14),
child: Container(
color: Colors.black,
child: Chewie(
controller: _chewieController!,
),
),
),
Positioned(
top: 12,
right: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle, size: 16, color: Colors.white),
Gap(4),
Text(
'Listo para subir',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
);
}
return Stack(
children: [
ClipRRect(