video thumbnails extractor añadido
This commit is contained in:
@@ -11,6 +11,7 @@ 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:nethive_neo/pages/videos/widgets/video_thumbnail_widget.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
class GestorVideosPage extends StatefulWidget {
|
||||
@@ -360,7 +361,12 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
_buildThumbnailPlaceholder(),
|
||||
)
|
||||
: _buildThumbnailPlaceholder(),
|
||||
: (video.fileUrl != null && video.fileUrl!.isNotEmpty)
|
||||
? VideoThumbnailWidget(
|
||||
videoUrl: video.fileUrl!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: _buildThumbnailPlaceholder(),
|
||||
// Overlay con icono de play
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
@@ -782,21 +788,26 @@ class _GestorVideosPageState extends State<GestorVideosPage> {
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppTheme.of(context).tertiaryBackground,
|
||||
AppTheme.of(context).primaryBackground,
|
||||
],
|
||||
: (video.fileUrl != null && video.fileUrl!.isNotEmpty)
|
||||
? VideoThumbnailWidget(
|
||||
videoUrl: video.fileUrl!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppTheme.of(context).tertiaryBackground,
|
||||
AppTheme.of(context).primaryBackground,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.video_library_rounded,
|
||||
size: 80,
|
||||
color: AppTheme.of(context).tertiaryText,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.video_library_rounded,
|
||||
size: 80,
|
||||
color: AppTheme.of(context).tertiaryText,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Overlay con gradiente
|
||||
Positioned.fill(
|
||||
|
||||
143
lib/pages/videos/widgets/video_thumbnail_widget.dart
Normal file
143
lib/pages/videos/widgets/video_thumbnail_widget.dart
Normal file
@@ -0,0 +1,143 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_thumbnail_video/index.dart';
|
||||
import 'package:get_thumbnail_video/video_thumbnail.dart';
|
||||
import 'package:nethive_neo/theme/theme.dart';
|
||||
|
||||
/// Widget que genera un thumbnail automático desde una URL de video.
|
||||
/// Se usa como fallback cuando no hay poster/portada subida por el usuario.
|
||||
/// Usa get_thumbnail_video para compatibilidad con Flutter Web y mejor rendimiento.
|
||||
class VideoThumbnailWidget extends StatefulWidget {
|
||||
final String videoUrl;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit fit;
|
||||
|
||||
const VideoThumbnailWidget({
|
||||
Key? key,
|
||||
required this.videoUrl,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit = BoxFit.cover,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<VideoThumbnailWidget> createState() => _VideoThumbnailWidgetState();
|
||||
}
|
||||
|
||||
class _VideoThumbnailWidgetState extends State<VideoThumbnailWidget> {
|
||||
Uint8List? _thumbnailBytes;
|
||||
bool _isLoading = true;
|
||||
bool _hasError = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_generateThumbnail();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(VideoThumbnailWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.videoUrl != widget.videoUrl) {
|
||||
_generateThumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _generateThumbnail() async {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_hasError = false;
|
||||
});
|
||||
|
||||
try {
|
||||
final thumbnail = await VideoThumbnail.thumbnailData(
|
||||
video: widget.videoUrl,
|
||||
imageFormat: ImageFormat.JPEG,
|
||||
maxWidth: 320,
|
||||
quality: 75,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (thumbnail != null && thumbnail.isNotEmpty) {
|
||||
setState(() {
|
||||
_thumbnailBytes = thumbnail;
|
||||
_isLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_hasError = true;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_hasError = true;
|
||||
_isLoading = false;
|
||||
});
|
||||
debugPrint('Error generando thumbnail: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Mostrar loading mientras genera thumbnail
|
||||
if (_isLoading) {
|
||||
return Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: AppTheme.of(context).tertiaryBackground,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppTheme.of(context).primaryColor.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Mostrar placeholder si hay error
|
||||
if (_hasError || _thumbnailBytes == null) {
|
||||
return Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: AppTheme.of(context).tertiaryBackground,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.video_library_rounded,
|
||||
size: 28,
|
||||
color: AppTheme.of(context).tertiaryText,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Mostrar thumbnail generado (imagen estática, no video)
|
||||
return Image.memory(
|
||||
_thumbnailBytes!,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: AppTheme.of(context).tertiaryBackground,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.video_library_rounded,
|
||||
size: 28,
|
||||
color: AppTheme.of(context).tertiaryText,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user