base creada
This commit is contained in:
634
lib/pages/videos/widgets/premium_upload_dialog.dart
Normal file
634
lib/pages/videos/widgets/premium_upload_dialog.dart
Normal file
@@ -0,0 +1,634 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.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';
|
||||
import 'package:nethive_neo/widgets/premium_button.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
class PremiumUploadDialog extends StatefulWidget {
|
||||
final VideosProvider provider;
|
||||
final VoidCallback onSuccess;
|
||||
|
||||
const PremiumUploadDialog({
|
||||
Key? key,
|
||||
required this.provider,
|
||||
required this.onSuccess,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PremiumUploadDialog> createState() => _PremiumUploadDialogState();
|
||||
}
|
||||
|
||||
class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
|
||||
final titleController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
MediaCategoryModel? selectedCategory;
|
||||
Uint8List? selectedVideo;
|
||||
String? videoFileName;
|
||||
Uint8List? selectedPoster;
|
||||
String? posterFileName;
|
||||
VideoPlayerController? _videoController;
|
||||
bool isUploading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
titleController.dispose();
|
||||
descriptionController.dispose();
|
||||
_videoController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectVideo() async {
|
||||
final result = await widget.provider.selectVideo();
|
||||
if (result) {
|
||||
setState(() {
|
||||
selectedVideo = widget.provider.webVideoBytes;
|
||||
videoFileName = widget.provider.videoName;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectPoster() async {
|
||||
final result = await widget.provider.selectPoster();
|
||||
if (result) {
|
||||
setState(() {
|
||||
selectedPoster = widget.provider.webPosterBytes;
|
||||
posterFileName = widget.provider.posterName;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _uploadVideo() async {
|
||||
if (titleController.text.isEmpty ||
|
||||
selectedCategory == null ||
|
||||
selectedVideo == null ||
|
||||
videoFileName == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Por favor completa los campos requeridos'),
|
||||
backgroundColor: const Color(0xFFFF2D2D),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => isUploading = true);
|
||||
|
||||
final success = await widget.provider.uploadVideo(
|
||||
title: titleController.text,
|
||||
description: descriptionController.text.isEmpty
|
||||
? null
|
||||
: descriptionController.text,
|
||||
categoryId: selectedCategory!.mediaCategoriesId,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() => isUploading = false);
|
||||
Navigator.pop(context);
|
||||
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.white),
|
||||
Gap(12),
|
||||
Text('Video subido exitosamente'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
);
|
||||
widget.onSuccess();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Row(
|
||||
children: [
|
||||
Icon(Icons.error, color: Colors.white),
|
||||
Gap(12),
|
||||
Text('Error al subir el video'),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFFFF2D2D),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = MediaQuery.of(context).size.width <= 800;
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
width: isMobile ? double.infinity : 900,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.9,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.of(context).secondaryBackground,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
|
||||
blurRadius: 40,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
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: [
|
||||
const Color(0xFF4EC9F5),
|
||||
const Color(0xFFFFB733),
|
||||
],
|
||||
),
|
||||
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.cloud_upload,
|
||||
color: Color(0xFF0B0B0D),
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Subir Nuevo Video',
|
||||
style: AppTheme.of(context).title2.override(
|
||||
fontFamily: 'Poppins',
|
||||
color: const Color(0xFF0B0B0D),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'Comparte tu contenido con el mundo',
|
||||
style: AppTheme.of(context).bodyText2.override(
|
||||
fontFamily: 'Poppins',
|
||||
color: const Color(0xFF0B0B0D).withOpacity(0.7),
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
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: [
|
||||
_buildFormFields(),
|
||||
const Gap(24),
|
||||
_buildPreviewSection(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormFields() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildLabel('Título del Video *'),
|
||||
const Gap(8),
|
||||
_buildTextField(
|
||||
controller: titleController,
|
||||
hintText: 'Ej: Tutorial de energía solar',
|
||||
prefixIcon: Icons.title,
|
||||
),
|
||||
const Gap(20),
|
||||
_buildLabel('Descripción'),
|
||||
const Gap(8),
|
||||
_buildTextField(
|
||||
controller: descriptionController,
|
||||
hintText: 'Describe el contenido del video...',
|
||||
prefixIcon: Icons.description,
|
||||
maxLines: 4,
|
||||
),
|
||||
const Gap(20),
|
||||
_buildLabel('Categoría *'),
|
||||
const Gap(8),
|
||||
_buildCategoryDropdown(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildLabel('Vista Previa'),
|
||||
const Gap(12),
|
||||
_buildVideoSelector(),
|
||||
const Gap(16),
|
||||
_buildPosterSelector(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
hint: Text(
|
||||
'Selecciona una categoría',
|
||||
style: AppTheme.of(context).bodyText1.override(
|
||||
fontFamily: 'Poppins',
|
||||
color: AppTheme.of(context).tertiaryText,
|
||||
),
|
||||
),
|
||||
dropdownColor: AppTheme.of(context).secondaryBackground,
|
||||
items: widget.provider.categories.map((category) {
|
||||
return DropdownMenuItem(
|
||||
value: category,
|
||||
child: Text(
|
||||
category.categoryName,
|
||||
style: AppTheme.of(context).bodyText1.override(
|
||||
fontFamily: 'Poppins',
|
||||
color: AppTheme.of(context).primaryText,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() => selectedCategory = value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoSelector() {
|
||||
return GestureDetector(
|
||||
onTap: _selectVideo,
|
||||
child: Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.of(context).tertiaryBackground,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: videoFileName != null
|
||||
? Colors.green.withOpacity(0.5)
|
||||
: AppTheme.of(context).primaryColor.withOpacity(0.3),
|
||||
width: 2,
|
||||
strokeAlign: BorderSide.strokeAlignInside,
|
||||
),
|
||||
),
|
||||
child: selectedVideo != null
|
||||
? _buildVideoPreview()
|
||||
: _buildUploadPlaceholder(
|
||||
icon: Icons.video_file,
|
||||
title: 'Seleccionar Video',
|
||||
subtitle: 'Click para elegir archivo',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPosterSelector() {
|
||||
return GestureDetector(
|
||||
onTap: _selectPoster,
|
||||
child: Container(
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.of(context).tertiaryBackground,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: posterFileName != null
|
||||
? Colors.green.withOpacity(0.5)
|
||||
: AppTheme.of(context).primaryColor.withOpacity(0.3),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: selectedPoster != null
|
||||
? _buildPosterPreview()
|
||||
: _buildUploadPlaceholder(
|
||||
icon: Icons.image,
|
||||
title: 'Miniatura (Opcional)',
|
||||
subtitle: 'Click para elegir imagen',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoPreview() {
|
||||
return Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.play_circle_outline,
|
||||
size: 64,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
const Gap(12),
|
||||
Text(
|
||||
videoFileName ?? 'Video seleccionado',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: 'Poppins',
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle, size: 16, color: Colors.white),
|
||||
Gap(4),
|
||||
Text(
|
||||
'Cargado',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontFamily: 'Poppins',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPosterPreview() {
|
||||
return Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Image.memory(
|
||||
selectedPoster!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle, size: 16, color: Colors.white),
|
||||
Gap(4),
|
||||
Text(
|
||||
'Cargado',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontFamily: 'Poppins',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadPlaceholder({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
}) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 40,
|
||||
color: AppTheme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
Text(
|
||||
title,
|
||||
style: AppTheme.of(context).bodyText1.override(
|
||||
fontFamily: 'Poppins',
|
||||
color: AppTheme.of(context).primaryText,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTheme.of(context).bodyText2.override(
|
||||
fontFamily: 'Poppins',
|
||||
color: AppTheme.of(context).tertiaryText,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: [
|
||||
PremiumButton(
|
||||
text: 'Cancelar',
|
||||
isOutlined: true,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
width: 120,
|
||||
),
|
||||
const Gap(12),
|
||||
PremiumButton(
|
||||
text: 'Subir Video',
|
||||
icon: Icons.cloud_upload,
|
||||
onPressed: _uploadVideo,
|
||||
isLoading: isUploading,
|
||||
width: 160,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user