[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(profile): video autoplay handling

+75 -18
+1 -1
lib/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart
··· 18 18 /// Color for the icon. Defaults to white for dark/overlay screens. 19 19 final Color color; 20 20 21 - /// Optional custom callback. If null, defaults to `context.router.maybePop()`. 21 + /// Optional custom callback. If null, defaults to `context.router.maybePop()` 22 22 final VoidCallback? onPressed; 23 23 24 24 @override
+3 -3
lib/src/core/design_system/templates/feeds_bar_template.dart
··· 4 4 5 5 /// The preferred height for the feeds bar content (excludes status bar). 6 6 /// The actual rendered height includes top safe area padding. 7 - /// This widget is designed for use with [Scaffold.extendBodyBehindAppBar] = true. 7 + /// This is designed for use with [Scaffold.extendBodyBehindAppBar] = true. 8 8 const kFeedsBarHeight = kToolbarHeight; 9 9 10 10 /// The width of the leading button area, matching [kToolbarHeight] like AppBar. ··· 41 41 final VoidCallback? onLeadingPressed; 42 42 43 43 /// Returns the toolbar height for layout calculations. 44 - /// The actual widget height includes status bar safe area padding. 45 - /// This matches [AppBar]'s behavior and works with [Scaffold.extendBodyBehindAppBar] = true. 44 + /// The actual widget height includes status bar safe area padding, 45 + /// matching [AppBar] & works with [Scaffold.extendBodyBehindAppBar] = true. 46 46 @override 47 47 Size get preferredSize => const Size.fromHeight(kFeedsBarHeight); 48 48
+19 -1
lib/src/features/feed/ui/widgets/videos/video_player.dart
··· 23 23 this.feed, 24 24 this.index, 25 25 this.profileFeedUri, 26 + this.isInitialPost = false, 26 27 }); 27 28 28 29 final String videoUrl; ··· 33 34 /// The profile URI for standalone profile feed visibility tracking. 34 35 /// When [index] provided, uses profile feed index provider not feed provider 35 36 final String? profileFeedUri; 37 + 38 + /// Whether this is the initial post that was clicked on. 39 + /// Used to trigger autoplay before the provider is fully initialized. 40 + final bool isInitialPost; 36 41 37 42 @override 38 43 ConsumerState<PostVideoPlayer> createState() => PostVideoPlayerState(); ··· 276 281 }); 277 282 } else if (profileFeedIndex != null && widget.index != null) { 278 283 // Profile feed visibility check 279 - if (_lastFeedIndex != profileFeedIndex) { 284 + if (profileFeedIndex == -1) { 285 + // Provider not initialized yet - use isInitialPost for initial autoplay 286 + if (widget.isInitialPost && _lastFeedIndex == null) { 287 + _lastFeedIndex = -1; // Mark as handled 288 + WidgetsBinding.instance.addPostFrameCallback((_) { 289 + if (mounted && !_userInteracted) { 290 + _handleAutoPlayPause( 291 + true, 292 + isFeedSettingsVisible: feedSettingsVisible, 293 + ); 294 + } 295 + }); 296 + } 297 + } else if (_lastFeedIndex != profileFeedIndex) { 280 298 _lastFeedIndex = profileFeedIndex; 281 299 WidgetsBinding.instance.addPostFrameCallback((_) { 282 300 if (mounted && !_userInteracted) {
+4 -1
lib/src/features/profile/providers/profile_feed_index_provider.dart
··· 4 4 5 5 /// Tracks the currently visible post index in a standalone profile feed. 6 6 /// Keyed by profile URI string to support multiple profiles. 7 + /// 8 + /// Returns -1 when not yet initialized. This prevents videos at index 0 from 9 + /// incorrectly auto-playing before the actual initial index is set. 7 10 @riverpod 8 11 class ProfileFeedIndex extends _$ProfileFeedIndex { 9 12 @override 10 13 int build(String profileUri) { 11 - return 0; 14 + return -1; // Not initialized 12 15 } 13 16 14 17 void setIndex(int index) {
+7 -4
lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart
··· 56 56 Widget build(BuildContext context) { 57 57 if (!_hasInitializedIndex) { 58 58 _hasInitializedIndex = true; 59 - WidgetsBinding.instance.addPostFrameCallback((_) { 60 - ref 61 - .read(profileFeedIndexProvider('likes:${widget.did}').notifier) 62 - .setIndex(widget.initialPostIndex); 59 + Future(() { 60 + if (mounted) { 61 + ref 62 + .read(profileFeedIndexProvider('likes:${widget.did}').notifier) 63 + .setIndex(widget.initialPostIndex); 64 + } 63 65 }); 64 66 } 65 67 ··· 129 131 videosOnly: false, 130 132 post: post, 131 133 index: index, 134 + isInitialPost: index == widget.initialPostIndex, 132 135 ); 133 136 }, 134 137 );
+7 -4
lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart
··· 60 60 Widget build(BuildContext context) { 61 61 if (!_hasInitializedIndex) { 62 62 _hasInitializedIndex = true; 63 - WidgetsBinding.instance.addPostFrameCallback((_) { 64 - ref 65 - .read(profileFeedIndexProvider(profileAtUri.toString()).notifier) 66 - .setIndex(widget.initialPostIndex); 63 + Future(() { 64 + if (mounted) { 65 + ref 66 + .read(profileFeedIndexProvider(profileAtUri.toString()).notifier) 67 + .setIndex(widget.initialPostIndex); 68 + } 67 69 }); 68 70 } 69 71 ··· 132 134 videosOnly: widget.videosOnly, 133 135 post: post, 134 136 index: index, 137 + isInitialPost: index == widget.initialPostIndex, 135 138 ); 136 139 }, 137 140 );
+7 -4
lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart
··· 56 56 Widget build(BuildContext context) { 57 57 if (!_hasInitializedIndex) { 58 58 _hasInitializedIndex = true; 59 - WidgetsBinding.instance.addPostFrameCallback((_) { 60 - ref 61 - .read(profileFeedIndexProvider('reposts:${widget.did}').notifier) 62 - .setIndex(widget.initialPostIndex); 59 + Future(() { 60 + if (mounted) { 61 + ref 62 + .read(profileFeedIndexProvider('reposts:${widget.did}').notifier) 63 + .setIndex(widget.initialPostIndex); 64 + } 63 65 }); 64 66 } 65 67 ··· 129 131 videosOnly: false, 130 132 post: post, 131 133 index: index, 134 + isInitialPost: index == widget.initialPostIndex, 132 135 ); 133 136 }, 134 137 );
+9
lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart
··· 24 24 super.key, 25 25 this.post, 26 26 this.index, 27 + this.isInitialPost = false, 27 28 }); 28 29 final AtUri postUri; 29 30 final AtUri profileUri; ··· 31 32 final PostView? post; 32 33 33 34 final int? index; 35 + 36 + /// Whether this is the initial post that was clicked on. 37 + /// Used to trigger autoplay before the provider is initialized. 38 + final bool isInitialPost; 34 39 35 40 @override 36 41 ConsumerState<ProfileFeedPostWidget> createState() => ··· 208 213 ? widget.profileUri.toString() 209 214 : null, 210 215 index: widget.index, 216 + isInitialPost: widget.isInitialPost, 211 217 ), 212 218 MediaViewBskyVideo() => PostVideoPlayer( 213 219 videoUrl: post.videoUrl, ··· 216 222 ? widget.profileUri.toString() 217 223 : null, 218 224 index: widget.index, 225 + isInitialPost: widget.isInitialPost, 219 226 ), 220 227 MediaViewImages() || MediaViewBskyImages() => ImageCarousel( 221 228 imageUrls: post.imageUrls, ··· 232 239 ? widget.profileUri.toString() 233 240 : null, 234 241 index: widget.index, 242 + isInitialPost: widget.isInitialPost, 235 243 ), 236 244 MediaViewBskyVideo() => PostVideoPlayer( 237 245 videoUrl: post.videoUrl, ··· 240 248 ? widget.profileUri.toString() 241 249 : null, 242 250 index: widget.index, 251 + isInitialPost: widget.isInitialPost, 243 252 ), 244 253 MediaViewImages() || 245 254 MediaViewBskyImages() => ImageCarousel(
+6
lib/src/features/profile/ui/widgets/profile_grid_widget.dart
··· 59 59 ]; 60 60 } 61 61 62 + // Add bottom padding to account for tab bar when on main navigation 63 + final bottomPadding = MediaQuery.of(context).padding.bottom + 64 + kBottomNavigationBarHeight; 65 + 62 66 return [ 63 67 SliverPadding( 64 68 padding: const EdgeInsets.all(5), ··· 89 93 ), 90 94 ), 91 95 ), 96 + // Bottom padding for tab bar 97 + SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)), 92 98 ]; 93 99 }, 94 100 loading: () => [
+6
lib/src/features/profile/ui/widgets/profile_likes_tab.dart
··· 101 101 ]; 102 102 } 103 103 104 + // Add bottom padding to account for tab bar when on main navigation 105 + final bottomPadding = MediaQuery.of(context).padding.bottom + 106 + kBottomNavigationBarHeight; 107 + 104 108 return [ 105 109 SliverPadding( 106 110 padding: const EdgeInsets.all(5), ··· 131 135 ), 132 136 ), 133 137 ), 138 + // Bottom padding for tab bar 139 + SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)), 134 140 ]; 135 141 }, 136 142 loading: () => [
+6
lib/src/features/profile/ui/widgets/profile_reposts_tab.dart
··· 101 101 ]; 102 102 } 103 103 104 + // Add bottom padding to account for tab bar when on main navigation 105 + final bottomPadding = MediaQuery.of(context).padding.bottom + 106 + kBottomNavigationBarHeight; 107 + 104 108 return [ 105 109 SliverPadding( 106 110 padding: const EdgeInsets.all(5), ··· 131 135 ), 132 136 ), 133 137 ), 138 + // Bottom padding for tab bar 139 + SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)), 134 140 ]; 135 141 }, 136 142 loading: () => [