[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(prefs): add toJson function

+83 -65
+4 -2
lib/src/core/design_system/components/organisms/side_action_bar.dart
··· 212 212 final soundCover = widget.soundCover; 213 213 if (soundCover != null && 214 214 soundCover.isNotEmpty && 215 - (soundCover.startsWith('http://') || soundCover.startsWith('https://'))) { 215 + (soundCover.startsWith('http://') || 216 + soundCover.startsWith('https://'))) { 216 217 children.addAll([ 217 218 const SizedBox(height: 13), 218 219 _SoundItem( ··· 336 337 const albumSize = 35.0; 337 338 338 339 // Don't render if cover URL is empty or invalid 339 - final hasValidCover = cover.isNotEmpty && 340 + final hasValidCover = 341 + cover.isNotEmpty && 340 342 (cover.startsWith('http://') || cover.startsWith('https://')); 341 343 342 344 return GestureDetector(
+4
lib/src/core/network/atproto/data/models/pref_models.dart
··· 101 101 .toList(); 102 102 return Preferences(preferences: preferences); 103 103 } 104 + 105 + Map<String, dynamic> toJson() => { 106 + 'preferences': preferences.map((e) => e.toJson()).toList(), 107 + }; 104 108 } 105 109 106 110 @Freezed(unionKey: r'$type')
+1 -1
lib/src/features/feed/ui/pages/feed_page.dart
··· 61 61 // during widget tree finalization 62 62 final notifier = _actionControllerNotifier; 63 63 if (notifier != null) { 64 - Future(() => notifier.clearController()); 64 + Future(notifier.clearController); 65 65 } 66 66 pageController.dispose(); 67 67 super.dispose();
+7 -8
lib/src/features/feed/ui/pages/feeds_page.dart
··· 39 39 if (_isPageControllerUpdating) return; 40 40 41 41 final activeIndex = feeds.indexOf(activeFeed); 42 - 42 + 43 43 // Always try to create controller if we don't have one and have feeds 44 44 if (_pageController == null && feeds.isNotEmpty) { 45 45 _pageController = PageController( ··· 50 50 51 51 if (activeIndex < 0 && feeds.isNotEmpty) { 52 52 // If active feed not in list but we have feeds, ensure controller exists 53 - if (_pageController == null) { 54 - _pageController = PageController(initialPage: 0); 55 - } 53 + _pageController ??= PageController(); 56 54 return; 57 55 } 58 56 ··· 118 116 119 117 // Ensure controller is created if we have feeds but controller is null 120 118 // This prevents the FeedsBar from disappearing during initialization 121 - // Also create it early if feeds list is still empty (handles transition from initial empty state) 119 + // Also create it early if feeds list still empty 120 + // (handles transition from initial empty state) 122 121 if (_pageController == null) { 123 122 if (feeds.isNotEmpty) { 124 123 final activeIndex = feeds.indexOf(activeFeed); ··· 126 125 initialPage: activeIndex >= 0 ? activeIndex : 0, 127 126 ); 128 127 } else { 129 - // Create controller even when feeds list is empty to keep FeedsBar visible 130 - // This handles the case when settings are still loading (feeds will populate soon) 131 - _pageController = PageController(initialPage: 0); 128 + // Create controller even when feeds list empty to keep FeedsBar visible 129 + // Handles case of settings still loading (feeds will populate soon) 130 + _pageController = PageController(); 132 131 } 133 132 } 134 133
+1 -1
lib/src/features/feed/ui/widgets/images/image_carousel.dart
··· 112 112 _revealedImages.add(index); 113 113 // Use TweenAnimationBuilder for smooth fade-in on first render 114 114 return TweenAnimationBuilder<double>( 115 - tween: Tween(begin: 0.0, end: 1.0), 115 + tween: Tween(begin: 0, end: 1), 116 116 duration: const Duration(milliseconds: 300), 117 117 curve: Curves.easeIn, 118 118 builder: (context, opacity, _) {
+9 -9
lib/src/features/feed/ui/widgets/post/feed_post_skeleton.dart
··· 52 52 crossAxisAlignment: CrossAxisAlignment.stretch, 53 53 children: [ 54 54 const Spacer(), 55 - Row( 55 + const Row( 56 56 crossAxisAlignment: CrossAxisAlignment.end, 57 57 children: [ 58 58 // Skeleton Info Bar (Left side) 59 - const Expanded(child: _SkeletonInfoBar()), 59 + Expanded(child: _SkeletonInfoBar()), 60 60 61 61 // Skeleton Side Action Bar (Right side) 62 62 Padding( 63 - padding: const EdgeInsets.only(right: 8, bottom: 8), 64 - child: const _SkeletonSideActionBar(), 63 + padding: EdgeInsets.only(right: 8, bottom: 8), 64 + child: _SkeletonSideActionBar(), 65 65 ), 66 66 ], 67 67 ), ··· 185 185 186 186 @override 187 187 Widget build(BuildContext context) { 188 - return Column( 188 + return const Column( 189 189 mainAxisSize: MainAxisSize.min, 190 190 children: [ 191 191 // Like button 192 192 _SkeletonActionItem(hasLabel: true), 193 - const SizedBox(height: 13), 193 + SizedBox(height: 13), 194 194 // Comment button 195 195 _SkeletonActionItem(hasLabel: true), 196 - const SizedBox(height: 13), 196 + SizedBox(height: 13), 197 197 // Repost button 198 198 _SkeletonActionItem(hasLabel: true), 199 - const SizedBox(height: 13), 199 + SizedBox(height: 13), 200 200 // Share button 201 - _SkeletonActionItem(hasLabel: false), 201 + _SkeletonActionItem(), 202 202 ], 203 203 ); 204 204 }
+4 -1
lib/src/features/feed/ui/widgets/post/post_overlay.dart
··· 91 91 Builder( 92 92 builder: (context) { 93 93 final informLabels = preferences != null 94 - ? LabelUtils.getInformLabels(preferences, labels) 94 + ? LabelUtils.getInformLabels( 95 + preferences, 96 + labels, 97 + ) 95 98 : <String>[]; 96 99 return InfoBar( 97 100 username: post.author.handle,
+3 -2
lib/src/features/feed/ui/widgets/videos/video_player.dart
··· 45 45 46 46 late AnimationController _bounceController; 47 47 late Animation<double> _bounceAnimation; 48 - 48 + 49 49 // For fade-in animation 50 50 late AnimationController _fadeController; 51 51 late Animation<double> _fadeAnimation; ··· 327 327 Positioned.fill( 328 328 child: FadeTransition( 329 329 opacity: _fadeAnimation, 330 - child: videoSize != null && videoSize.width > 0 && videoSize.height > 0 330 + child: 331 + videoSize != null && videoSize.width > 0 && videoSize.height > 0 331 332 ? FittedBox( 332 333 fit: fitMode, 333 334 child: SizedBox(
+7 -6
lib/src/features/profile/providers/blocks_provider.dart
··· 34 34 List<ProfileView> profiles, 35 35 ) async { 36 36 // Check if profile is incomplete - need to check multiple fields 37 - // A profile is incomplete if it's missing displayName, description, or avatar 38 - // AND has a valid handle (if handle is missing, it's likely a deleted account) 37 + // Profile is incomplete if it's missing displayName, description, or avatar 38 + // AND has a valid handle (if handle missing, it's likely deleted account) 39 39 final didsToFetch = profiles 40 40 .where((profile) { 41 - // Profile is incomplete if it has a valid handle but is missing key fields 42 - final hasValidHandle = profile.handle.isNotEmpty && 43 - profile.handle != 'unknown.invalid'; 44 - final isIncomplete = hasValidHandle && 41 + // Profile incomplete if it has valid handle but missing key fields 42 + final hasValidHandle = 43 + profile.handle.isNotEmpty && profile.handle != 'unknown.invalid'; 44 + final isIncomplete = 45 + hasValidHandle && 45 46 (profile.displayName == null || 46 47 profile.description == null || 47 48 profile.avatar == null);
+7 -6
lib/src/features/profile/providers/user_list_provider.dart
··· 85 85 List<ProfileView> profiles, 86 86 ) async { 87 87 // Check if profile is incomplete - need to check multiple fields 88 - // A profile is incomplete if it's missing displayName, description, or avatar 89 - // AND has a valid handle (if handle is missing, it's likely a deleted account) 88 + // Profile is incomplete if it's missing displayName, description, or avatar 89 + // AND has a valid handle (if handle missing, it's likely a deleted account) 90 90 final didsToFetch = profiles 91 91 .where((profile) { 92 - // Profile is incomplete if it has a valid handle but is missing key fields 93 - final hasValidHandle = profile.handle.isNotEmpty && 94 - profile.handle != 'unknown.invalid'; 95 - final isIncomplete = hasValidHandle && 92 + // Profile is incomplete if it has valid handle but missing key fields 93 + final hasValidHandle = 94 + profile.handle.isNotEmpty && profile.handle != 'unknown.invalid'; 95 + final isIncomplete = 96 + hasValidHandle && 96 97 (profile.displayName == null || 97 98 profile.description == null || 98 99 profile.avatar == null);
+9 -10
lib/src/features/profile/ui/widgets/profile_feed_post_widget.dart
··· 219 219 ), 220 220 MediaViewImages() || MediaViewBskyImages() => ImageCarousel( 221 221 imageUrls: post.imageUrls, 222 - hasKnownInteractions: post.viewer?.knownInteractions != 223 - null && 222 + hasKnownInteractions: 223 + post.viewer?.knownInteractions != null && 224 224 post.viewer!.knownInteractions!.isNotEmpty, 225 225 ), 226 226 MediaViewBskyRecordWithMedia(:final media) => ··· 241 241 : null, 242 242 index: widget.index, 243 243 ), 244 - MediaViewImages() || MediaViewBskyImages() => 245 - ImageCarousel( 246 - imageUrls: post.imageUrls, 247 - hasKnownInteractions: post.viewer 248 - ?.knownInteractions != 249 - null && 250 - post.viewer!.knownInteractions!.isNotEmpty, 251 - ), 244 + MediaViewImages() || 245 + MediaViewBskyImages() => ImageCarousel( 246 + imageUrls: post.imageUrls, 247 + hasKnownInteractions: 248 + post.viewer?.knownInteractions != null && 249 + post.viewer!.knownInteractions!.isNotEmpty, 250 + ), 252 251 _ => const DecoratedBox( 253 252 decoration: BoxDecoration(color: AppColors.black), 254 253 ),
+1 -3
lib/src/features/profile/ui/widgets/profile_grid_widget.dart
··· 200 200 borderRadius: BorderRadius.circular(15), 201 201 ), 202 202 child: SvgPicture.asset( 203 - postSource == 'bsky' 204 - ? 'images/bsky.svg' 205 - : 'images/sprk.svg', 203 + postSource == 'bsky' ? 'images/bsky.svg' : 'images/sprk.svg', 206 204 width: 12, 207 205 height: 12, 208 206 package: 'assets',
+6 -3
lib/src/features/search/ui/pages/post_results.dart
··· 182 182 } 183 183 184 184 final post = state.searchResults[index]; 185 - final preferences = 186 - ref.read(userPreferencesProvider).asData?.value; 185 + final preferences = ref 186 + .read(userPreferencesProvider) 187 + .asData 188 + ?.value; 187 189 final labels = post.labels ?? []; 188 - final shouldBlur = preferences != null && 190 + final shouldBlur = 191 + preferences != null && 189 192 labels.isNotEmpty && 190 193 LabelUtils.shouldBlurContent(preferences, labels); 191 194
+20 -13
lib/src/features/settings/providers/settings_provider.dart
··· 39 39 /// Tracks if settings have been loaded to prevent resetting state on rebuild 40 40 bool _hasLoadedSettings = false; 41 41 42 - /// Tracks if loadSettings is currently in progress to prevent concurrent calls 42 + /// Tracks if loadSettings currently in progress to prevent concurrent calls 43 43 bool _isLoadingSettings = false; 44 44 45 45 SettingsState? _preservedState; ··· 51 51 SparkLogger get logger => 52 52 _logger ??= GetIt.instance<LogService>().getLogger('Settings'); 53 53 Feed get defaultFeed => _defaultFeed ??= Feed( 54 - type: 'timeline', 55 - config: SavedFeed(type: 'timeline', value: 'following', pinned: true), 56 - ); 54 + type: 'timeline', 55 + config: SavedFeed(type: 'timeline', value: 'following', pinned: true), 56 + ); 57 57 58 58 String get _defaultModServiceDid { 59 59 // Extract DID part from modDid (remove fragment if present) ··· 77 77 /// Updates preferences through the UserPreferences provider. 78 78 /// This ensures all watchers are notified of changes. 79 79 Future<void> _updatePreferences(Preferences preferences) async { 80 - await ref.read(userPreferencesProvider.notifier).updatePreferences( 81 - preferences, 82 - ); 80 + await ref 81 + .read(userPreferencesProvider.notifier) 82 + .updatePreferences( 83 + preferences, 84 + ); 83 85 } 84 86 85 87 @override ··· 87 89 // Watch the preferences provider - when it updates, we'll rebuild 88 90 ref.watch(userPreferencesProvider); 89 91 90 - // Preserve state across rebuilds to prevent the feeds tabs from disappearing 92 + // Preserve state across rebuilds to prevent feeds tabs from disappearing 91 93 listenSelf((previous, next) { 92 94 _preservedState = next; 93 95 }); ··· 119 121 return; 120 122 } 121 123 122 - // If already loaded, skip (but allow explicit refresh via syncPreferencesFromServer) 124 + // Skip if already loaded 125 + // (but allow explicit refresh via syncPreferencesFromServer) 123 126 if (_hasLoadedSettings) { 124 127 logger.d('Settings already loaded, skipping'); 125 128 return; ··· 456 459 return labelers; 457 460 } 458 461 459 - /// Ensures all label values from all subscribed labelers have preferences set. 460 - /// Only fetches policies for labelers that haven't been checked yet this session. 462 + /// Ensures all label values from all subscribed labelers have prefs set. 463 + /// Only fetch policies for labelers that haven't been checked this session. 461 464 Future<void> _ensureAllLabelersPoliciesSet(List<String> labelerDids) async { 462 465 final uncheckedLabelers = labelerDids 463 466 .where((did) => !_labelerPoliciesChecked.contains(did)) ··· 500 503 Preference.labelersPref(labelers: updatedLabelers), 501 504 ); 502 505 503 - await _updatePreferences(Preferences(preferences: updatedPreferencesList)); 506 + await _updatePreferences( 507 + Preferences(preferences: updatedPreferencesList), 508 + ); 504 509 logger.d('Labeler added successfully: $did'); 505 510 506 511 // Fetch and set default label preferences for this labeler ··· 542 547 Preference.labelersPref(labelers: updatedLabelers), 543 548 ); 544 549 545 - await _updatePreferences(Preferences(preferences: updatedPreferencesList)); 550 + await _updatePreferences( 551 + Preferences(preferences: updatedPreferencesList), 552 + ); 546 553 logger.d('Labeler removed successfully: $did'); 547 554 } catch (e) { 548 555 logger.e('Error removing labeler: $e');