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

@@ -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(