[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.

feat(settings): persist active feed in local storage

+87 -20
+16 -4
lib/src/features/feed/ui/pages/feeds_page.dart
··· 59 59 } 60 60 61 61 if (_pageController == null) return; 62 - if (!_pageController!.hasClients) return; 62 + if (!_pageController!.hasClients) { 63 + WidgetsBinding.instance.addPostFrameCallback((_) { 64 + if (!mounted || 65 + _pageController == null || 66 + !_pageController!.hasClients) { 67 + return; 68 + } 69 + _updatePageController(feeds, activeFeed, forceJump: forceJump); 70 + }); 71 + return; 72 + } 63 73 64 74 final currentPage = _pageController!.page?.round() ?? 0; 65 75 if ((currentPage != activeIndex || forceJump) && activeIndex >= 0) { ··· 124 134 if (needsInitialization || activeFeedChanged || feedsOrderChanged) { 125 135 // Force jump when order changes to ensure we stay on the active feed 126 136 _updatePageController(feeds, activeFeed, forceJump: feedsOrderChanged); 127 - _isInitialized = true; 128 - _lastActiveFeed = activeFeed; 129 - _lastFeedsList = List.from(feeds); // Create a copy 137 + if (_pageController != null && _pageController!.hasClients) { 138 + _isInitialized = true; 139 + _lastActiveFeed = activeFeed; 140 + _lastFeedsList = List.from(feeds); // Create a copy 141 + } 130 142 } 131 143 132 144 // Ensure controller is created if we have feeds but controller is null
+1 -1
lib/src/features/feed/ui/widgets/videos/video_player.dart
··· 282 282 } else if (profileFeedIndex != null && widget.index != null) { 283 283 // Profile feed visibility check 284 284 if (profileFeedIndex == -1) { 285 - // Provider not initialized yet - use isInitialPost for initial autoplay 285 + // Provider not initialized, use isInitialPost for initial autoplay 286 286 if (widget.isInitialPost && _lastFeedIndex == null) { 287 287 _lastFeedIndex = -1; // Mark as handled 288 288 WidgetsBinding.instance.addPostFrameCallback((_) {
-1
lib/src/features/notifications/ui/pages/notifications_page.dart
··· 1 1 import 'package:auto_route/auto_route.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 - import 'package:spark/src/core/ui/foundation/colors.dart'; 5 4 import 'package:spark/src/features/notifications/providers/notification_provider.dart' 6 5 show notificationProvider; 7 6 import 'package:spark/src/features/notifications/ui/widgets/notifications_list.dart';
+1 -2
lib/src/features/profile/providers/profile_feed_provider.dart
··· 98 98 // 2. API returned posts but all were duplicates (prevents infinite loops) 99 99 // Note: Don't check posts.isEmpty - empty page with cursor means more exist 100 100 final isEndOfNetwork = 101 - result.cursor == null || 102 - (result.posts.isNotEmpty && newPosts.isEmpty); 101 + result.cursor == null || (result.posts.isNotEmpty && newPosts.isEmpty); 103 102 104 103 return ProfileFeedState( 105 104 loadedPosts: allPosts,
+1 -2
lib/src/features/profile/providers/profile_likes_provider.dart
··· 92 92 // 2. API returned posts but all were duplicates (prevents infinite loops) 93 93 // Note: Don't check posts.isEmpty - empty page with cursor means more exist 94 94 final isEndOfNetwork = 95 - result.cursor == null || 96 - (result.posts.isNotEmpty && newPosts.isEmpty); 95 + result.cursor == null || (result.posts.isNotEmpty && newPosts.isEmpty); 97 96 98 97 return ProfileFeedState( 99 98 loadedPosts: allPosts,
+1 -2
lib/src/features/profile/providers/profile_reposts_provider.dart
··· 92 92 // 2. API returned posts but all were duplicates (prevents infinite loops) 93 93 // Note: Don't check posts.isEmpty - empty page with cursor means more exist 94 94 final isEndOfNetwork = 95 - result.cursor == null || 96 - (result.posts.isNotEmpty && newPosts.isEmpty); 95 + result.cursor == null || (result.posts.isNotEmpty && newPosts.isEmpty); 97 96 98 97 return ProfileFeedState( 99 98 loadedPosts: allPosts,
+2 -2
lib/src/features/profile/ui/widgets/profile_grid_widget.dart
··· 60 60 } 61 61 62 62 // Add bottom padding to account for tab bar when on main navigation 63 - final bottomPadding = MediaQuery.of(context).padding.bottom + 64 - kBottomNavigationBarHeight; 63 + final bottomPadding = 64 + MediaQuery.of(context).padding.bottom + kBottomNavigationBarHeight; 65 65 66 66 return [ 67 67 SliverPadding(
+2 -2
lib/src/features/profile/ui/widgets/profile_likes_tab.dart
··· 102 102 } 103 103 104 104 // Add bottom padding to account for tab bar when on main navigation 105 - final bottomPadding = MediaQuery.of(context).padding.bottom + 106 - kBottomNavigationBarHeight; 105 + final bottomPadding = 106 + MediaQuery.of(context).padding.bottom + kBottomNavigationBarHeight; 107 107 108 108 return [ 109 109 SliverPadding(
+2 -2
lib/src/features/profile/ui/widgets/profile_reposts_tab.dart
··· 102 102 } 103 103 104 104 // Add bottom padding to account for tab bar when on main navigation 105 - final bottomPadding = MediaQuery.of(context).padding.bottom + 106 - kBottomNavigationBarHeight; 105 + final bottomPadding = 106 + MediaQuery.of(context).padding.bottom + kBottomNavigationBarHeight; 107 107 108 108 return [ 109 109 SliverPadding(
+61 -2
lib/src/features/settings/providers/settings_provider.dart
··· 8 8 import 'package:spark/src/core/network/atproto/data/repositories/pref_repository.dart'; 9 9 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 10 10 import 'package:spark/src/core/storage/preferences/default_preferences.dart'; 11 + import 'package:spark/src/core/storage/preferences/storage_manager.dart'; 11 12 import 'package:spark/src/core/utils/logging/log_service.dart'; 12 13 import 'package:spark/src/core/utils/logging/logger.dart'; 13 14 import 'package:spark/src/features/settings/providers/preferences_provider.dart'; ··· 55 56 config: SavedFeed(type: 'timeline', value: 'following', pinned: true), 56 57 ); 57 58 59 + /// Storage key for the last active feed, unique per user (DID) 60 + String get _activeFeedStorageKey { 61 + final did = sprkRepository.authRepository.did ?? 'anonymous'; 62 + return 'active_feed_$did'; 63 + } 64 + 58 65 String get _defaultModServiceDid { 59 66 // Extract DID part from modDid (remove fragment if present) 60 67 final modDid = sprkRepository.modDid; ··· 84 91 ); 85 92 } 86 93 94 + /// Loads the last active feed from local storage 95 + /// Returns null if no feed is saved or if loading fails 96 + Future<Feed?> _loadLastActiveFeedFromStorage() async { 97 + try { 98 + final storage = GetIt.instance<StorageManager>().preferences; 99 + final json = await storage.getObject<Map<String, dynamic>>( 100 + _activeFeedStorageKey, 101 + ); 102 + if (json != null) { 103 + final feed = Feed.fromJson(json); 104 + logger.d('Loaded saved active feed: ${feed.config.value}'); 105 + return feed; 106 + } 107 + } catch (e) { 108 + logger.w('Error loading saved active feed: $e'); 109 + } 110 + return null; 111 + } 112 + 113 + /// Saves the active feed to local storage 114 + Future<void> _saveActiveFeedToStorage(Feed feed) async { 115 + try { 116 + final storage = GetIt.instance<StorageManager>().preferences; 117 + await storage.setObject(_activeFeedStorageKey, feed.toJson()); 118 + logger.d('Saved active feed to storage: ${feed.config.value}'); 119 + } catch (e) { 120 + logger.w('Error saving active feed: $e'); 121 + } 122 + } 123 + 87 124 @override 88 125 SettingsState build() { 89 126 // Note: We intentionally don't watch userPreferencesProvider here. ··· 180 217 likedFeeds: likedFeeds, 181 218 ); 182 219 _hasLoadedSettings = true; 220 + 221 + // Save the default active feed to storage 222 + await _saveActiveFeedToStorage(updatedActiveFeed); 183 223 return; 184 224 } catch (e) { 185 225 logger.e('Error setting default preferences: $e'); ··· 189 229 190 230 // Hydrate feeds with generator views using getFeedGenerators 191 231 final feeds = await feedRepository.getFeedsFromSavedFeeds(savedFeeds); 192 - final activeFeed = _getActiveFeedFromFeeds(feeds, savedFeeds); 232 + 233 + // Try to load the last active feed from local storage 234 + final savedActiveFeed = await _loadLastActiveFeedFromStorage(); 235 + 236 + // Determine active feed: use saved feed if it still exists in feeds list, 237 + // otherwise fall back to server preferences (first pinned) 238 + final Feed activeFeed; 239 + if (savedActiveFeed != null && 240 + savedActiveFeed.config.pinned && 241 + feeds.any((f) => f.config.id == savedActiveFeed.config.id)) { 242 + activeFeed = feeds.firstWhere( 243 + (f) => f.config.id == savedActiveFeed.config.id, 244 + ); 245 + logger.d( 246 + 'Restored last active feed from storage: ${activeFeed.config.value}', 247 + ); 248 + } else { 249 + activeFeed = _getActiveFeedFromFeeds(feeds, savedFeeds); 250 + } 193 251 194 252 logger.d( 195 253 'Settings loaded - activeFeed: ${activeFeed.config.value}, ' ··· 375 433 await _updateFeedsInPreferences(updatedList); 376 434 } 377 435 378 - /// Sets selected feed index 436 + /// Sets selected feed index and saves to local storage 379 437 Future<void> setActiveFeed(Feed feed) async { 380 438 state = state.copyWith(activeFeed: feed); 439 + await _saveActiveFeedToStorage(feed); 381 440 } 382 441 383 442 /// Debug method to reload settings and verify persistence