diff --git a/lib/src/core/media/create_media_actions.dart b/lib/src/core/media/create_media_actions.dart index 1c6e99a7..a28c42cc 100644 --- a/lib/src/core/media/create_media_actions.dart +++ b/lib/src/core/media/create_media_actions.dart @@ -2,9 +2,11 @@ import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:image_picker/image_picker.dart'; import 'package:pro_video_editor/pro_video_editor.dart'; +import 'package:spark/src/core/media/media_playback_suspension_provider.dart'; import 'package:spark/src/core/pro_video_editor/models/video_editor_result.dart'; import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository.dart'; import 'package:spark/src/core/routing/app_router.dart'; @@ -52,37 +54,48 @@ class CreateMediaActions { if (pickedVideo == null) return; if (!context.mounted) return; - final editorVideo = EditorVideo.file(File(pickedVideo.path)); - final repository = GetIt.I(); - VideoEditorResult? result; - if (storyMode) { - if (!context.mounted) return; - result = await repository.openStoryVideoEditor(context, editorVideo); - } else { - if (!context.mounted) return; - result = await repository.openVideoEditor(context, editorVideo); - } - - if (result != null && context.mounted) { + final mediaPlaybackContainer = ProviderScope.containerOf( + context, + listen: false, + ); + final mediaPlaybackSuspension = suspendMediaPlayback( + mediaPlaybackContainer, + ); + try { + final editorVideo = EditorVideo.file(File(pickedVideo.path)); + final repository = GetIt.I(); + VideoEditorResult? result; if (storyMode) { - // For stories, post directly - await context.router.push( - StoryPostRoute( - videoPath: result.video.path, - soundRef: result.soundRef, - embeds: result.embeds, - ), - ); + if (!context.mounted) return; + result = await repository.openStoryVideoEditor(context, editorVideo); } else { - // For posts, go to review - await context.router.push( - VideoReviewRoute( - videoPath: result.video.path, - storyMode: storyMode, - soundRef: result.soundRef, - ), - ); + if (!context.mounted) return; + result = await repository.openVideoEditor(context, editorVideo); + } + + if (result != null && context.mounted) { + if (storyMode) { + // For stories, post directly + await context.router.push( + StoryPostRoute( + videoPath: result.video.path, + soundRef: result.soundRef, + embeds: result.embeds, + ), + ); + } else { + // For posts, go to review + await context.router.push( + VideoReviewRoute( + videoPath: result.video.path, + storyMode: storyMode, + soundRef: result.soundRef, + ), + ); + } } + } finally { + mediaPlaybackSuspension.release(); } }; } diff --git a/lib/src/core/media/media_playback_suspension_provider.dart b/lib/src/core/media/media_playback_suspension_provider.dart new file mode 100644 index 00000000..c956f6cc --- /dev/null +++ b/lib/src/core/media/media_playback_suspension_provider.dart @@ -0,0 +1,33 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; + +final _mediaPlaybackSuspensionCountProvider = StateProvider((ref) => 0); + +final mediaPlaybackSuspendedProvider = Provider( + (ref) => ref.watch(_mediaPlaybackSuspensionCountProvider) > 0, +); + +MediaPlaybackSuspension suspendMediaPlayback(ProviderContainer container) { + final controller = container.read( + _mediaPlaybackSuspensionCountProvider.notifier, + ); + controller.state += 1; + return MediaPlaybackSuspension._(container); +} + +class MediaPlaybackSuspension { + MediaPlaybackSuspension._(this._container); + + final ProviderContainer _container; + bool _released = false; + + void release() { + if (_released) return; + _released = true; + + final controller = _container.read( + _mediaPlaybackSuspensionCountProvider.notifier, + ); + controller.state -= 1; + } +} diff --git a/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart b/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart index 4bc24cfb..292fa9b5 100644 --- a/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart +++ b/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart @@ -3,10 +3,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; -import 'package:spark/src/core/routing/app_router.dart'; +import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/ui/widgets/content_warning_overlay.dart'; import 'package:spark/src/core/ui/widgets/heart_animation.dart'; import 'package:spark/src/core/utils/label_utils.dart'; diff --git a/lib/src/features/feed/ui/widgets/post/post_media_viewer.dart b/lib/src/features/feed/ui/widgets/post/post_media_viewer.dart index 6f510334..34fef381 100644 --- a/lib/src/features/feed/ui/widgets/post/post_media_viewer.dart +++ b/lib/src/features/feed/ui/widgets/post/post_media_viewer.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/media/media_playback_suspension_provider.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_video_aspect_ratio.dart'; import 'package:spark/src/core/pro_video_editor/models/sound_audio_track.dart'; @@ -9,7 +11,7 @@ import 'package:spark/src/features/feed/ui/widgets/images/image_carousel.dart'; import 'package:spark/src/features/feed/ui/widgets/post/static_media_sound_player.dart'; import 'package:spark/src/features/feed/ui/widgets/videos/video_player.dart'; -class PostMediaViewer extends StatefulWidget { +class PostMediaViewer extends ConsumerStatefulWidget { const PostMediaViewer({ required this.post, required this.isActive, @@ -20,10 +22,10 @@ class PostMediaViewer extends StatefulWidget { final bool isActive; @override - State createState() => PostMediaViewerState(); + ConsumerState createState() => PostMediaViewerState(); } -class PostMediaViewerState extends State { +class PostMediaViewerState extends ConsumerState { final GlobalKey _videoPlayerKey = GlobalKey(); final StaticMediaSoundController _staticSoundController = @@ -43,13 +45,15 @@ class PostMediaViewerState extends State { @override Widget build(BuildContext context) { final post = widget.post; + final mediaPlaybackSuspended = ref.watch(mediaPlaybackSuspendedProvider); + final isMediaActive = widget.isActive && !mediaPlaybackSuspended; if (post.videoUrl.isNotEmpty) { return PostVideoPlayer( key: _videoPlayerKey, videoUrl: post.videoUrl, thumbnail: post.thumbnailUrl, - isActive: widget.isActive, + isActive: isMediaActive, videoAspectRatio: post.videoAspectRatio, ); } @@ -59,7 +63,7 @@ class PostMediaViewerState extends State { return StaticMediaSoundPlayer( audioUrl: sound == null ? null : playableAudioUrl(sound), mimeType: sound == null ? null : audioMimeType(sound), - shouldPlay: widget.isActive, + shouldPlay: isMediaActive, controller: _staticSoundController, child: ImageCarousel( imageUrls: post.imageUrls, diff --git a/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart b/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart index 3baf2f72..26d1592f 100644 --- a/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart +++ b/lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart @@ -5,8 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; -import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/ui/widgets/content_warning_overlay.dart'; import 'package:spark/src/core/ui/widgets/heart_animation.dart'; import 'package:spark/src/core/utils/label_utils.dart'; diff --git a/test/src/core/media/media_playback_suspension_provider_test.dart b/test/src/core/media/media_playback_suspension_provider_test.dart new file mode 100644 index 00000000..ada94ce6 --- /dev/null +++ b/test/src/core/media/media_playback_suspension_provider_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spark/src/core/media/media_playback_suspension_provider.dart'; + +void main() { + test('tracks nested media playback suspensions', () { + final container = ProviderContainer(); + addTearDown(container.dispose); + + expect(container.read(mediaPlaybackSuspendedProvider), isFalse); + + final firstSuspension = suspendMediaPlayback(container); + expect(container.read(mediaPlaybackSuspendedProvider), isTrue); + + final secondSuspension = suspendMediaPlayback(container); + firstSuspension.release(); + firstSuspension.release(); + expect(container.read(mediaPlaybackSuspendedProvider), isTrue); + + secondSuspension.release(); + expect(container.read(mediaPlaybackSuspendedProvider), isFalse); + }); + + test('release is idempotent', () { + final container = ProviderContainer(); + addTearDown(container.dispose); + + final suspension = suspendMediaPlayback(container); + suspension.release(); + suspension.release(); + + expect(container.read(mediaPlaybackSuspendedProvider), isFalse); + }); +}