fix gestor de contenido, wip reproductor y diseño
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 49 KiB |
BIN
assets/images/logo_nh1.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 30 KiB |
BIN
assets/images/logo_nh_b1.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
@@ -12,7 +12,7 @@ const String apiGatewayUrl = 'https://cbl.cbluna-dev.com/uapi/lu/api';
|
|||||||
const String n8nUrl = 'https://u-n8n.cbluna-dev.com/webhook';
|
const String n8nUrl = 'https://u-n8n.cbluna-dev.com/webhook';
|
||||||
const bearerApiGateway = "Basic YWlyZmxvdzpjYiF1bmEyMDIz";
|
const bearerApiGateway = "Basic YWlyZmxvdzpjYiF1bmEyMDIz";
|
||||||
|
|
||||||
const int organizationId = 10;
|
const int organizationId = 17;
|
||||||
const String lectoresUrl = 'https://lectoresurbanos.com/';
|
const String lectoresUrl = 'https://lectoresurbanos.com/';
|
||||||
|
|
||||||
const themeId = String.fromEnvironment('themeId', defaultValue: '2');
|
const themeId = String.fromEnvironment('themeId', defaultValue: '2');
|
||||||
|
|||||||
@@ -153,4 +153,9 @@ class MediaFileModel {
|
|||||||
DateTime? get lastViewedAt => metadataJson?['last_viewed_at'] != null
|
DateTime? get lastViewedAt => metadataJson?['last_viewed_at'] != null
|
||||||
? DateTime.tryParse(metadataJson!['last_viewed_at'])
|
? DateTime.tryParse(metadataJson!['last_viewed_at'])
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Poster information from metadata_json
|
||||||
|
String? get posterUrl => metadataJson?['poster_url'];
|
||||||
|
String? get posterFileName => metadataJson?['poster_file_name'];
|
||||||
|
int? get fileSizeBytesFromMetadata => metadataJson?['file_size_bytes'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,45 +145,26 @@ class _LoginFormState extends State<LoginForm> with TickerProviderStateMixin {
|
|||||||
margin: const EdgeInsets.only(bottom: 50),
|
margin: const EdgeInsets.only(bottom: 50),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
SizedBox(
|
||||||
width: 50,
|
width: 75,
|
||||||
height: 50,
|
height: 75,
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(0xFF3B82F6),
|
|
||||||
Color(0xFF10B981)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: const Color(0xFF3B82F6)
|
|
||||||
.withOpacity(0.3),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/favicon.png',
|
'assets/images/favicon.png',
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Bienvenido a NetHive',
|
'Bienvenido a EnergyMedia',
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
fontSize: 28,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
@@ -200,17 +181,6 @@ class _LoginFormState extends State<LoginForm> with TickerProviderStateMixin {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
|
||||||
Container(
|
|
||||||
width: 60,
|
|
||||||
height: 3,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [Color(0xFF10B981), Color(0xFF3B82F6)],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -262,7 +232,7 @@ class _LoginFormState extends State<LoginForm> with TickerProviderStateMixin {
|
|||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'admin@nethive.com',
|
hintText: 'admin@energymedia.com',
|
||||||
hintStyle: GoogleFonts.inter(
|
hintStyle: GoogleFonts.inter(
|
||||||
color: isMobile
|
color: isMobile
|
||||||
? Colors.white.withOpacity(0.6)
|
? Colors.white.withOpacity(0.6)
|
||||||
|
|||||||
@@ -251,6 +251,9 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
rendererContext.row.cells['video']?.value as MediaFileModel?;
|
rendererContext.row.cells['video']?.value as MediaFileModel?;
|
||||||
if (video == null) return const SizedBox();
|
if (video == null) return const SizedBox();
|
||||||
|
|
||||||
|
// Obtener poster desde metadata_json
|
||||||
|
final posterUrl = video.posterUrl;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.all(4),
|
margin: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -259,9 +262,9 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: video.fileUrl != null
|
child: posterUrl != null && posterUrl.isNotEmpty
|
||||||
? Image.network(
|
? Image.network(
|
||||||
video.fileUrl!,
|
posterUrl,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) => Icon(
|
errorBuilder: (context, error, stackTrace) => Icon(
|
||||||
Icons.video_library,
|
Icons.video_library,
|
||||||
@@ -285,8 +288,8 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
width: 250,
|
width: 250,
|
||||||
),
|
),
|
||||||
PlutoColumn(
|
PlutoColumn(
|
||||||
title: 'Archivo',
|
title: 'Descripción',
|
||||||
field: 'fileName',
|
field: 'file_description',
|
||||||
type: PlutoColumnType.text(),
|
type: PlutoColumnType.text(),
|
||||||
width: 200,
|
width: 200,
|
||||||
),
|
),
|
||||||
@@ -381,7 +384,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (video.fileUrl != null)
|
// Mostrar poster si existe, sino mostrar placeholder
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(12),
|
topLeft: Radius.circular(12),
|
||||||
@@ -389,8 +392,9 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: Image.network(
|
child: video.posterUrl != null && video.posterUrl!.isNotEmpty
|
||||||
video.fileUrl!,
|
? Image.network(
|
||||||
|
video.posterUrl!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) => Container(
|
errorBuilder: (context, error, stackTrace) => Container(
|
||||||
color: AppTheme.of(context).tertiaryBackground,
|
color: AppTheme.of(context).tertiaryBackground,
|
||||||
@@ -400,6 +404,14 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
color: AppTheme.of(context).tertiaryText,
|
color: AppTheme.of(context).tertiaryText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
color: AppTheme.of(context).tertiaryBackground,
|
||||||
|
child: Icon(
|
||||||
|
Icons.video_library,
|
||||||
|
size: 64,
|
||||||
|
color: AppTheme.of(context).tertiaryText,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -586,7 +598,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
.where((cat) => cat.mediaCategoriesId == video.mediaCategoryFk)
|
.where((cat) => cat.mediaCategoriesId == video.mediaCategoryFk)
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
|
|
||||||
await showDialog(
|
final result = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
builder: (context, setDialogState) => AlertDialog(
|
builder: (context, setDialogState) => AlertDialog(
|
||||||
@@ -671,7 +683,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context, false),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Cancelar',
|
'Cancelar',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -681,8 +693,6 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(context);
|
|
||||||
|
|
||||||
// Actualizar campos
|
// Actualizar campos
|
||||||
if (titleController.text != video.title) {
|
if (titleController.text != video.title) {
|
||||||
await provider.updateVideoTitle(
|
await provider.updateVideoTitle(
|
||||||
@@ -707,16 +717,7 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
Navigator.pop(context, true);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Video actualizado exitosamente'),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _loadData();
|
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppTheme.of(context).primaryColor,
|
backgroundColor: AppTheme.of(context).primaryColor,
|
||||||
@@ -728,6 +729,20 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Manejar resultado después de cerrar el diálogo
|
||||||
|
if (result == true) {
|
||||||
|
await _loadData();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Video actualizado exitosamente'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteVideo(
|
Future<void> _deleteVideo(
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ class _VideosLayoutState extends State<VideosLayout> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(bool isMobile) {
|
Widget _buildHeader(bool isMobile) {
|
||||||
|
final isDark = AppTheme.themeMode == ThemeMode.dark;
|
||||||
|
final isLightBackground = !isDark;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.all(isMobile ? 16 : 24),
|
padding: EdgeInsets.all(isMobile ? 16 : 24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -82,26 +85,15 @@ class _VideosLayoutState extends State<VideosLayout> {
|
|||||||
color: AppTheme.of(context).primaryText,
|
color: AppTheme.of(context).primaryText,
|
||||||
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
|
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
|
||||||
),
|
),
|
||||||
Container(
|
// Logo de EnergyMedia
|
||||||
padding: const EdgeInsets.all(8),
|
Image.asset(
|
||||||
decoration: BoxDecoration(
|
isMobile
|
||||||
gradient: AppTheme.of(context).primaryGradient,
|
? 'assets/images/favicon.png'
|
||||||
borderRadius: BorderRadius.circular(8),
|
: isLightBackground
|
||||||
),
|
? 'assets/images/logo_nh.png'
|
||||||
child: const Icon(
|
: 'assets/images/logo_nh_b.png',
|
||||||
Icons.energy_savings_leaf,
|
height: isMobile ? 32 : 75,
|
||||||
color: Color(0xFF0B0B0D),
|
fit: BoxFit.contain,
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Text(
|
|
||||||
'EnergyMedia',
|
|
||||||
style: AppTheme.of(context).title2.override(
|
|
||||||
fontFamily: 'Poppins',
|
|
||||||
color: AppTheme.of(context).primaryText,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
@@ -154,29 +146,13 @@ class _VideosLayoutState extends State<VideosLayout> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
// Logo de EnergyMedia
|
||||||
padding: const EdgeInsets.all(12),
|
Image.asset(
|
||||||
decoration: BoxDecoration(
|
'assets/images/logo_nh_b.png',
|
||||||
color: Colors.white.withOpacity(0.2),
|
height: 50,
|
||||||
borderRadius: BorderRadius.circular(12),
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
const Gap(12),
|
||||||
Icons.energy_savings_leaf,
|
|
||||||
color: Color(0xFF0B0B0D),
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
Text(
|
|
||||||
'EnergyMedia',
|
|
||||||
style: AppTheme.of(context).title2.override(
|
|
||||||
fontFamily: 'Poppins',
|
|
||||||
color: const Color(0xFF0B0B0D),
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
Text(
|
||||||
'Content Manager',
|
'Content Manager',
|
||||||
style: AppTheme.of(context).bodyText2.override(
|
style: AppTheme.of(context).bodyText2.override(
|
||||||
@@ -436,28 +412,13 @@ class _VideosLayoutState extends State<VideosLayout> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
// Logo de EnergyMedia
|
||||||
padding: const EdgeInsets.all(12),
|
Image.asset(
|
||||||
decoration: BoxDecoration(
|
'assets/images/logo_nh.png',
|
||||||
color: Colors.white.withOpacity(0.2),
|
height: 45,
|
||||||
borderRadius: BorderRadius.circular(12),
|
fit: BoxFit.contain,
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.energy_savings_leaf,
|
|
||||||
color: Color(0xFF0B0B0D),
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Text(
|
|
||||||
'EnergyMedia',
|
|
||||||
style: AppTheme.of(context).title2.override(
|
|
||||||
fontFamily: 'Poppins',
|
|
||||||
color: const Color(0xFF0B0B0D),
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
Text(
|
||||||
'Content Manager',
|
'Content Manager',
|
||||||
style: AppTheme.of(context).bodyText2.override(
|
style: AppTheme.of(context).bodyText2.override(
|
||||||
|
|||||||
@@ -131,24 +131,42 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
videosRows.add(
|
videosRows.add(
|
||||||
PlutoRow(
|
PlutoRow(
|
||||||
cells: {
|
cells: {
|
||||||
'id': PlutoCell(value: media.mediaFileId),
|
'video': PlutoCell(value: media), // Objeto completo para renderers
|
||||||
'thumbnail':
|
'thumbnail': PlutoCell(value: media.fileUrl),
|
||||||
PlutoCell(value: media.fileUrl), // Para mostrar thumbnail
|
|
||||||
'title': PlutoCell(value: media.title ?? media.fileName),
|
'title': PlutoCell(value: media.title ?? media.fileName),
|
||||||
'description': PlutoCell(value: media.fileDescription ?? ''),
|
'file_description': PlutoCell(value: media.fileDescription),
|
||||||
'category':
|
'category':
|
||||||
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
|
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
|
||||||
'reproducciones': PlutoCell(value: media.reproducciones),
|
'reproducciones': PlutoCell(value: media.reproducciones),
|
||||||
'duration': PlutoCell(value: media.seconds ?? 0),
|
'duration': PlutoCell(
|
||||||
'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)),
|
value: media.seconds != null
|
||||||
'created_at': PlutoCell(value: media.createdAt),
|
? _formatDuration(media.seconds!)
|
||||||
'actions': PlutoCell(value: media.mediaFileId),
|
: '-'),
|
||||||
|
'createdAt': PlutoCell(
|
||||||
|
value: media.createdAt?.toString().split('.')[0] ?? '-'),
|
||||||
|
'actions': PlutoCell(value: media),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format duration in seconds to human readable
|
||||||
|
String _formatDuration(int seconds) {
|
||||||
|
final duration = Duration(seconds: seconds);
|
||||||
|
final hours = duration.inHours;
|
||||||
|
final minutes = duration.inMinutes.remainder(60);
|
||||||
|
final secs = duration.inSeconds.remainder(60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return '${hours}h ${minutes}m';
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return '${minutes}m ${secs}s';
|
||||||
|
} else {
|
||||||
|
return '${secs}s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get category name by ID
|
/// Get category name by ID
|
||||||
String _getCategoryName(int? categoryId) {
|
String _getCategoryName(int? categoryId) {
|
||||||
if (categoryId == null) return 'Sin categoría';
|
if (categoryId == null) return 'Sin categoría';
|
||||||
@@ -258,21 +276,24 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
.from('energymedia')
|
.from('energymedia')
|
||||||
.getPublicUrl(videoStoragePath!);
|
.getPublicUrl(videoStoragePath!);
|
||||||
|
|
||||||
// 3. Upload poster if exists
|
// 3. Upload poster if exists (solo storage, no DB)
|
||||||
int? posterFileId;
|
String? posterUrlUploaded;
|
||||||
if (webPosterBytes != null && posterName != null) {
|
if (webPosterBytes != null && posterName != null) {
|
||||||
posterFileId = await _uploadPoster();
|
posterUrlUploaded = await _uploadPoster();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Create media_files record
|
// 4. Create media_files record (UN SOLO REGISTRO con poster en metadata_json)
|
||||||
final metadataJson = {
|
final metadataJson = {
|
||||||
'uploaded_at': DateTime.now().toIso8601String(),
|
'uploaded_at': DateTime.now().toIso8601String(),
|
||||||
'reproducciones': 0,
|
'reproducciones': 0,
|
||||||
'original_file_name': videoName,
|
'original_file_name': videoName,
|
||||||
'duration_seconds': durationSeconds,
|
'duration_seconds': durationSeconds,
|
||||||
|
'file_size_bytes': webVideoBytes!.length, // Peso del video
|
||||||
|
if (posterUrlUploaded != null) 'poster_url': posterUrlUploaded,
|
||||||
|
if (posterUrlUploaded != null) 'poster_file_name': posterName,
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await supabaseML.from('media_files').insert({
|
await supabaseML.from('media_files').insert({
|
||||||
'file_name': fileName,
|
'file_name': fileName,
|
||||||
'title': title,
|
'title': title,
|
||||||
'file_description': description,
|
'file_description': description,
|
||||||
@@ -288,16 +309,7 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
'seconds': durationSeconds,
|
'seconds': durationSeconds,
|
||||||
'is_public_file': true,
|
'is_public_file': true,
|
||||||
'uploaded_by_user_id': currentUser?.id,
|
'uploaded_by_user_id': currentUser?.id,
|
||||||
}).select();
|
|
||||||
|
|
||||||
// 5. Create poster relationship if exists
|
|
||||||
if (posterFileId != null && response.isNotEmpty) {
|
|
||||||
final mediaFileId = response[0]['media_file_id'];
|
|
||||||
await supabaseML.from('media_posters').insert({
|
|
||||||
'media_file_id': mediaFileId,
|
|
||||||
'poster_file_id': posterFileId,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
_clearUploadState();
|
_clearUploadState();
|
||||||
@@ -317,8 +329,9 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload poster image (internal helper)
|
/// Upload poster image to storage only (NO database record)
|
||||||
Future<int?> _uploadPoster() async {
|
/// Returns the public URL of the uploaded poster
|
||||||
|
Future<String?> _uploadPoster() async {
|
||||||
if (webPosterBytes == null || posterName == null) return null;
|
if (webPosterBytes == null || posterName == null) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -326,6 +339,7 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
final fileName = '${timestamp}_$posterName';
|
final fileName = '${timestamp}_$posterName';
|
||||||
posterStoragePath = 'imagenes/$fileName';
|
posterStoragePath = 'imagenes/$fileName';
|
||||||
|
|
||||||
|
// Solo subir al storage, NO crear registro en media_files
|
||||||
await supabaseML.storage.from('energymedia').uploadBinary(
|
await supabaseML.storage.from('energymedia').uploadBinary(
|
||||||
posterStoragePath!,
|
posterStoragePath!,
|
||||||
webPosterBytes!,
|
webPosterBytes!,
|
||||||
@@ -335,26 +349,12 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Obtener URL pública del poster
|
||||||
posterUrl = supabaseML.storage
|
posterUrl = supabaseML.storage
|
||||||
.from('energymedia')
|
.from('energymedia')
|
||||||
.getPublicUrl(posterStoragePath!);
|
.getPublicUrl(posterStoragePath!);
|
||||||
|
|
||||||
// Create media_files record for poster
|
return posterUrl; // Retornar solo la URL, no el ID
|
||||||
final response = await supabaseML.from('media_files').insert({
|
|
||||||
'file_name': fileName,
|
|
||||||
'title': 'Poster',
|
|
||||||
'file_type': 'image',
|
|
||||||
'mime_type': _getMimeType(posterFileExtension),
|
|
||||||
'file_extension': posterFileExtension,
|
|
||||||
'file_size_bytes': webPosterBytes!.length,
|
|
||||||
'file_url': posterUrl,
|
|
||||||
'storage_path': posterStoragePath,
|
|
||||||
'organization_fk': organizationId,
|
|
||||||
'is_public_file': true,
|
|
||||||
'uploaded_by_user_id': currentUser?.id,
|
|
||||||
}).select();
|
|
||||||
|
|
||||||
return response[0]['media_file_id'] as int;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error en _uploadPoster: $e');
|
print('Error en _uploadPoster: $e');
|
||||||
return null;
|
return null;
|
||||||
@@ -500,23 +500,30 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
final storagePath = response['storage_path'] as String?;
|
final storagePath = response['storage_path'] as String?;
|
||||||
|
final metadataJson = response['metadata_json'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
// Delete from storage if path exists
|
// Delete video from storage if path exists
|
||||||
if (storagePath != null) {
|
if (storagePath != null) {
|
||||||
await supabaseML.storage.from('energymedia').remove([storagePath]);
|
await supabaseML.storage.from('energymedia').remove([storagePath]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete associated posters
|
// Delete poster from storage if exists in metadata_json
|
||||||
final posters = await supabaseML
|
if (metadataJson != null && metadataJson['poster_url'] != null) {
|
||||||
.from('media_posters')
|
final posterUrl = metadataJson['poster_url'] as String;
|
||||||
.select('poster_file_id')
|
// Extraer el path del storage desde la URL
|
||||||
.eq('media_file_id', mediaFileId);
|
// URL format: https://xxx.supabase.co/storage/v1/object/public/energymedia/imagenes/filename.png
|
||||||
|
final uri = Uri.parse(posterUrl);
|
||||||
|
final pathSegments = uri.pathSegments;
|
||||||
|
|
||||||
for (var poster in posters) {
|
// Encontrar el índice después de 'energymedia' y construir el path
|
||||||
await _deletePosterFile(poster['poster_file_id']);
|
final bucketIndex = pathSegments.indexOf('energymedia');
|
||||||
|
if (bucketIndex != -1 && bucketIndex < pathSegments.length - 1) {
|
||||||
|
final posterPath = pathSegments.sublist(bucketIndex + 1).join('/');
|
||||||
|
await supabaseML.storage.from('energymedia').remove([posterPath]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete database record (cascade will delete posters relationship)
|
// Delete database record
|
||||||
await supabaseML
|
await supabaseML
|
||||||
.from('media_files')
|
.from('media_files')
|
||||||
.delete()
|
.delete()
|
||||||
@@ -537,30 +544,6 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete poster file (internal helper)
|
|
||||||
Future<void> _deletePosterFile(int posterFileId) async {
|
|
||||||
try {
|
|
||||||
final response = await supabaseML
|
|
||||||
.from('media_files')
|
|
||||||
.select('storage_path')
|
|
||||||
.eq('media_file_id', posterFileId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
final storagePath = response['storage_path'] as String?;
|
|
||||||
|
|
||||||
if (storagePath != null) {
|
|
||||||
await supabaseML.storage.from('energymedia').remove([storagePath]);
|
|
||||||
}
|
|
||||||
|
|
||||||
await supabaseML
|
|
||||||
.from('media_files')
|
|
||||||
.delete()
|
|
||||||
.eq('media_file_id', posterFileId);
|
|
||||||
} catch (e) {
|
|
||||||
print('Error en _deletePosterFile: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== ANALYTICS METHODS ==========
|
// ========== ANALYTICS METHODS ==========
|
||||||
|
|
||||||
/// Increment view count
|
/// Increment view count
|
||||||
@@ -659,17 +642,20 @@ class VideosProvider extends ChangeNotifier {
|
|||||||
videosRows.add(
|
videosRows.add(
|
||||||
PlutoRow(
|
PlutoRow(
|
||||||
cells: {
|
cells: {
|
||||||
'id': PlutoCell(value: media.mediaFileId),
|
'video': PlutoCell(value: media),
|
||||||
'thumbnail': PlutoCell(value: media.fileUrl),
|
'thumbnail': PlutoCell(value: media.fileUrl),
|
||||||
'title': PlutoCell(value: media.title ?? media.fileName),
|
'title': PlutoCell(value: media.title ?? media.fileName),
|
||||||
'description': PlutoCell(value: media.fileDescription ?? ''),
|
'fileName': PlutoCell(value: media.fileName),
|
||||||
'category':
|
'category':
|
||||||
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
|
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
|
||||||
'reproducciones': PlutoCell(value: media.reproducciones),
|
'reproducciones': PlutoCell(value: media.reproducciones),
|
||||||
'duration': PlutoCell(value: media.seconds ?? 0),
|
'duration': PlutoCell(
|
||||||
'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)),
|
value: media.seconds != null
|
||||||
'created_at': PlutoCell(value: media.createdAt),
|
? _formatDuration(media.seconds!)
|
||||||
'actions': PlutoCell(value: media.mediaFileId),
|
: '-'),
|
||||||
|
'createdAt': PlutoCell(
|
||||||
|
value: media.createdAt?.toString().split('.')[0] ?? '-'),
|
||||||
|
'actions': PlutoCell(value: media),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||