mp4 changed, just a thumbnail

master
gitea 2 months ago
parent 222515a2b5
commit 5a2d287d00

@ -1799,25 +1799,25 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeVideo(); _extractThumbnail();
} }
Future<void> _initializeVideo() async { Future<void> _extractThumbnail() async {
try { try {
_controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
await _controller!.initialize(); await _controller!.initialize();
if (mounted) { if (mounted && _controller != null) {
// Seek to first frame and pause
await _controller!.seekTo(Duration.zero);
_controller!.pause();
setState(() { setState(() {
_isInitialized = true; _isInitialized = true;
}); });
// Play continuously in a loop
_controller!.setLooping(true);
_controller!.play();
} }
} catch (e) { } catch (e) {
Logger.warning('Failed to initialize video thumbnail: $e'); Logger.warning('Failed to extract video thumbnail: $e');
if (mounted) { if (mounted) {
setState(() { setState(() {
_hasError = true; _hasError = true;
@ -1848,6 +1848,21 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
), ),
), ),
), ),
// Transparent play button overlay
Center(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.4),
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow,
size: 48,
color: Colors.white,
),
),
),
], ],
); );
} }
@ -1855,23 +1870,22 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
return Stack( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
// Video thumbnail (first frame)
AspectRatio( AspectRatio(
aspectRatio: _controller!.value.aspectRatio, aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!), child: VideoPlayer(_controller!),
), ),
// Play icon overlay // Transparent play button overlay centered
Positioned( Center(
top: 4,
right: 4,
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6), color: Colors.black.withValues(alpha: 0.5),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
Icons.play_circle_filled, Icons.play_arrow,
size: 24, size: 48,
color: Colors.white, color: Colors.white,
), ),
), ),

@ -859,25 +859,25 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeVideo(); _extractThumbnail();
} }
Future<void> _initializeVideo() async { Future<void> _extractThumbnail() async {
try { try {
_controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
await _controller!.initialize(); await _controller!.initialize();
if (mounted) { if (mounted && _controller != null) {
// Seek to first frame and pause
await _controller!.seekTo(Duration.zero);
_controller!.pause();
setState(() { setState(() {
_isInitialized = true; _isInitialized = true;
}); });
// Play continuously in a loop
_controller!.setLooping(true);
_controller!.play();
} }
} catch (e) { } catch (e) {
Logger.warning('Failed to initialize video thumbnail: $e'); Logger.warning('Failed to extract video thumbnail: $e');
if (mounted) { if (mounted) {
setState(() { setState(() {
_hasError = true; _hasError = true;
@ -908,6 +908,21 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
), ),
), ),
), ),
// Transparent play button overlay
Center(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.4),
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow,
size: 48,
color: Colors.white,
),
),
),
], ],
); );
} }
@ -915,23 +930,22 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
return Stack( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
// Video thumbnail (first frame)
AspectRatio( AspectRatio(
aspectRatio: _controller!.value.aspectRatio, aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!), child: VideoPlayer(_controller!),
), ),
// Play icon overlay // Transparent play button overlay centered
Positioned( Center(
top: 4,
right: 4,
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6), color: Colors.black.withValues(alpha: 0.5),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
Icons.play_circle_filled, Icons.play_arrow,
size: 24, size: 48,
color: Colors.white, color: Colors.white,
), ),
), ),

@ -1085,25 +1085,25 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeVideo(); _extractThumbnail();
} }
Future<void> _initializeVideo() async { Future<void> _extractThumbnail() async {
try { try {
_controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
await _controller!.initialize(); await _controller!.initialize();
if (mounted) { if (mounted && _controller != null) {
// Seek to first frame and pause
await _controller!.seekTo(Duration.zero);
_controller!.pause();
setState(() { setState(() {
_isInitialized = true; _isInitialized = true;
}); });
// Play continuously in a loop
_controller!.setLooping(true);
_controller!.play();
} }
} catch (e) { } catch (e) {
Logger.warning('Failed to initialize video thumbnail: $e'); Logger.warning('Failed to extract video thumbnail: $e');
if (mounted) { if (mounted) {
setState(() { setState(() {
_hasError = true; _hasError = true;
@ -1134,6 +1134,21 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
), ),
), ),
), ),
// Transparent play button overlay
Center(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.4),
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow,
size: 48,
color: Colors.white,
),
),
),
], ],
); );
} }
@ -1141,23 +1156,22 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
return Stack( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
// Video thumbnail (first frame)
AspectRatio( AspectRatio(
aspectRatio: _controller!.value.aspectRatio, aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!), child: VideoPlayer(_controller!),
), ),
// Play icon overlay // Transparent play button overlay centered
Positioned( Center(
top: 4,
right: 4,
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6), color: Colors.black.withValues(alpha: 0.5),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
Icons.play_circle_filled, Icons.play_arrow,
size: 24, size: 48,
color: Colors.white, color: Colors.white,
), ),
), ),

@ -645,7 +645,6 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
onTap: () => _setDefaultServer(server), onTap: () => _setDefaultServer(server),
); );
}), }),
const Divider(),
// Add server button // Add server button
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),

@ -220,6 +220,59 @@ void main() {
throwsA(isA<Exception>()), throwsA(isA<Exception>()),
); );
}); });
test('creates a recipe with video URLs', () async {
final recipe = RecipeModel(
id: 'test-recipe-video',
title: 'Recipe with Video',
tags: ['video'],
imageUrls: ['https://example.com/image.jpg'],
videoUrls: ['https://example.com/video.mp4'],
);
final created = await recipeService.createRecipe(recipe);
expect(created.videoUrls, equals(['https://example.com/video.mp4']));
expect(created.videoUrls.length, equals(1));
});
test('updates a recipe with video URLs', () async {
final recipe = RecipeModel(
id: 'test-recipe-video-update',
title: 'Original Recipe',
tags: ['test'],
videoUrls: ['https://example.com/video1.mp4'],
);
await recipeService.createRecipe(recipe);
final updated = recipe.copyWith(
videoUrls: ['https://example.com/video1.mp4', 'https://example.com/video2.mp4'],
);
final result = await recipeService.updateRecipe(updated);
expect(result.videoUrls.length, equals(2));
expect(result.videoUrls, contains('https://example.com/video1.mp4'));
expect(result.videoUrls, contains('https://example.com/video2.mp4'));
});
test('retrieves recipe with video URLs from database', () async {
final recipe = RecipeModel(
id: 'test-recipe-video-retrieve',
title: 'Recipe with Videos',
tags: ['test'],
videoUrls: ['https://example.com/video1.mp4', 'https://example.com/video2.mp4'],
);
await recipeService.createRecipe(recipe);
final retrieved = await recipeService.getRecipe('test-recipe-video-retrieve');
expect(retrieved, isNotNull);
expect(retrieved!.videoUrls.length, equals(2));
expect(retrieved.videoUrls, contains('https://example.com/video1.mp4'));
expect(retrieved.videoUrls, contains('https://example.com/video2.mp4'));
});
}); });
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.