diff --git a/lib/ui/add_recipe/add_recipe_screen.dart b/lib/ui/add_recipe/add_recipe_screen.dart index 279c646..00dad9a 100644 --- a/lib/ui/add_recipe/add_recipe_screen.dart +++ b/lib/ui/add_recipe/add_recipe_screen.dart @@ -1799,25 +1799,25 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> { @override void initState() { super.initState(); - _initializeVideo(); + _extractThumbnail(); } - Future _initializeVideo() async { + Future _extractThumbnail() async { try { _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); await _controller!.initialize(); - if (mounted) { + if (mounted && _controller != null) { + // Seek to first frame and pause + await _controller!.seekTo(Duration.zero); + _controller!.pause(); + setState(() { _isInitialized = true; }); - - // Play continuously in a loop - _controller!.setLooping(true); - _controller!.play(); } } catch (e) { - Logger.warning('Failed to initialize video thumbnail: $e'); + Logger.warning('Failed to extract video thumbnail: $e'); if (mounted) { setState(() { _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( fit: StackFit.expand, children: [ + // Video thumbnail (first frame) AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), ), - // Play icon overlay - Positioned( - top: 4, - right: 4, + // Transparent play button overlay centered + Center( child: Container( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.6), + color: Colors.black.withValues(alpha: 0.5), shape: BoxShape.circle, ), child: const Icon( - Icons.play_circle_filled, - size: 24, + Icons.play_arrow, + size: 48, color: Colors.white, ), ), diff --git a/lib/ui/favourites/favourites_screen.dart b/lib/ui/favourites/favourites_screen.dart index 9feab0c..45e8771 100644 --- a/lib/ui/favourites/favourites_screen.dart +++ b/lib/ui/favourites/favourites_screen.dart @@ -859,25 +859,25 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> { @override void initState() { super.initState(); - _initializeVideo(); + _extractThumbnail(); } - Future _initializeVideo() async { + Future _extractThumbnail() async { try { _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); await _controller!.initialize(); - if (mounted) { + if (mounted && _controller != null) { + // Seek to first frame and pause + await _controller!.seekTo(Duration.zero); + _controller!.pause(); + setState(() { _isInitialized = true; }); - - // Play continuously in a loop - _controller!.setLooping(true); - _controller!.play(); } } catch (e) { - Logger.warning('Failed to initialize video thumbnail: $e'); + Logger.warning('Failed to extract video thumbnail: $e'); if (mounted) { setState(() { _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( fit: StackFit.expand, children: [ + // Video thumbnail (first frame) AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), ), - // Play icon overlay - Positioned( - top: 4, - right: 4, + // Transparent play button overlay centered + Center( child: Container( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.6), + color: Colors.black.withValues(alpha: 0.5), shape: BoxShape.circle, ), child: const Icon( - Icons.play_circle_filled, - size: 24, + Icons.play_arrow, + size: 48, color: Colors.white, ), ), diff --git a/lib/ui/recipes/recipes_screen.dart b/lib/ui/recipes/recipes_screen.dart index e9f364f..3ca5a90 100644 --- a/lib/ui/recipes/recipes_screen.dart +++ b/lib/ui/recipes/recipes_screen.dart @@ -1085,25 +1085,25 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> { @override void initState() { super.initState(); - _initializeVideo(); + _extractThumbnail(); } - Future _initializeVideo() async { + Future _extractThumbnail() async { try { _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); await _controller!.initialize(); - if (mounted) { + if (mounted && _controller != null) { + // Seek to first frame and pause + await _controller!.seekTo(Duration.zero); + _controller!.pause(); + setState(() { _isInitialized = true; }); - - // Play continuously in a loop - _controller!.setLooping(true); - _controller!.play(); } } catch (e) { - Logger.warning('Failed to initialize video thumbnail: $e'); + Logger.warning('Failed to extract video thumbnail: $e'); if (mounted) { setState(() { _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( fit: StackFit.expand, children: [ + // Video thumbnail (first frame) AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), ), - // Play icon overlay - Positioned( - top: 4, - right: 4, + // Transparent play button overlay centered + Center( child: Container( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.6), + color: Colors.black.withValues(alpha: 0.5), shape: BoxShape.circle, ), child: const Icon( - Icons.play_circle_filled, - size: 24, + Icons.play_arrow, + size: 48, color: Colors.white, ), ), diff --git a/lib/ui/relay_management/relay_management_screen.dart b/lib/ui/relay_management/relay_management_screen.dart index cc44761..6afa059 100644 --- a/lib/ui/relay_management/relay_management_screen.dart +++ b/lib/ui/relay_management/relay_management_screen.dart @@ -645,7 +645,6 @@ class _RelayManagementScreenState extends State { onTap: () => _setDefaultServer(server), ); }), - const Divider(), // Add server button Padding( padding: const EdgeInsets.all(16.0), diff --git a/test/data/recipes/recipe_service_test.dart b/test/data/recipes/recipe_service_test.dart index 70b2716..c844959 100644 --- a/test/data/recipes/recipe_service_test.dart +++ b/test/data/recipes/recipe_service_test.dart @@ -220,6 +220,59 @@ void main() { throwsA(isA()), ); }); + + 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')); + }); }); }