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

chore: remove beta indicators at app store request

+40 -186
+7 -3
lib/src/core/auth/data/repositories/onboarding_repository_impl.dart
··· 30 30 31 31 @override 32 32 Future<bool> hasSparkProfile() async { 33 - if (_did == null) return false; 33 + await _authRepository.initializationComplete; 34 + 35 + if (_did == null || _did!.isEmpty) { 36 + return false; 37 + } 34 38 35 39 final uri = AtUri.parse('at://$_did/so.sprk.actor.profile/self'); 36 40 try { ··· 38 42 _logger.i('Spark profile found: ${response.record.toJson()}'); 39 43 return response.record.toJson().isNotEmpty; 40 44 } catch (e) { 41 - // Treat 404 and 'Could not locate record' 400 errors as no profile 45 + // Treat explicit "record not found" failures as no profile. 42 46 final msg = e.toString().toLowerCase(); 43 47 if (msg.contains('404') || 44 48 msg.contains('could not locate record') || 45 - msg.contains('400')) { 49 + msg.contains('record not found')) { 46 50 return false; 47 51 } 48 52 _logger.e('Error checking Spark profile', error: e);
+29 -6
lib/src/core/routing/app_router.dart
··· 9 9 import 'package:spark/src/core/auth/data/repositories/onboarding_repository.dart'; 10 10 import 'package:spark/src/core/network/atproto/atproto.dart'; 11 11 import 'package:spark/src/core/routing/pages.dart'; 12 + import 'package:spark/src/core/utils/logging/log_service.dart'; 13 + import 'package:spark/src/core/utils/logging/logger.dart'; 12 14 import 'package:spark/src/features/profile/ui/pages/user_list_page.dart'; 13 15 14 16 part 'app_router.gr.dart'; 15 17 16 18 class AuthGuard extends AutoRouteGuard { 19 + final SparkLogger _logger = GetIt.instance<LogService>().getLogger( 20 + 'AuthGuard', 21 + ); 22 + 17 23 @override 18 24 Future<void> onNavigation( 19 25 NavigationResolver resolver, ··· 23 29 final onboardingRepository = GetIt.instance<OnboardingRepository>(); 24 30 25 31 try { 32 + await authRepository.initializationComplete; 33 + final isSessionValid = await authRepository.validateSession(); 34 + 35 + if (!isSessionValid) { 36 + _logger.i('Redirecting to register because the user is not signed in'); 37 + resolver.redirectUntil(const RegisterRoute()); 38 + return; 39 + } 40 + 26 41 final hasSpark = await onboardingRepository.hasSparkProfile(); 27 42 28 43 if (!hasSpark) { 29 - resolver.redirectUntil(const RegisterRoute()); 44 + _logger.i( 45 + 'Redirecting authenticated user to onboarding because ' 46 + 'their Spark profile is missing', 47 + ); 48 + resolver.redirectUntil(const OnboardingRoute()); 30 49 return; 31 50 } 32 51 33 - final isSessionValid = await authRepository.validateSession(); 52 + resolver.next(); 53 + } catch (e, stackTrace) { 54 + _logger.e( 55 + 'Auth guard failed while resolving navigation', 56 + error: e, 57 + stackTrace: stackTrace, 58 + ); 34 59 35 - if (!isSessionValid) { 36 - resolver.redirectUntil(const LoginRoute()); 60 + if (authRepository.isAuthenticated) { 61 + resolver.next(); 37 62 return; 38 63 } 39 64 40 - resolver.next(); 41 - } catch (e) { 42 65 resolver.redirectUntil(const RegisterRoute()); 43 66 } 44 67 }
+1 -1
lib/src/features/auth/ui/pages/login_page.dart
··· 103 103 104 104 if (!mounted) return; 105 105 106 - context.router.replaceAll([const FeedsRoute()]); 106 + context.router.replaceAll([const MainRoute()]); 107 107 } else { 108 108 context.router.replaceAll([const OnboardingRoute()]); 109 109 }
+3 -4
lib/src/features/auth/ui/pages/register_page.dart
··· 83 83 84 84 if (!mounted) return; 85 85 86 - context.router.replaceAll([const FeedsRoute()]); 86 + context.router.replaceAll([const MainRoute()]); 87 87 } else { 88 88 context.router.replaceAll([const OnboardingRoute()]); 89 89 } ··· 171 171 ), 172 172 const SizedBox(height: 12), 173 173 Text( 174 - 'Spark is currently in public beta.\n' 175 - 'We value your feedback and are still \n' 176 - 'rapidly improving.', 174 + 'Share videos, connect with friends,\n' 175 + 'and take back your timeline.', 177 176 style: AppTypography.textMediumMedium.copyWith( 178 177 color: colorScheme.onSurfaceVariant, 179 178 height: 1.5,
-7
lib/src/features/settings/providers/settings_provider.dart
··· 429 429 await _saveActiveFeedToStorage(feed); 430 430 } 431 431 432 - /// Debug method to reload settings and verify persistence 433 - Future<void> reloadSettingsForTesting() async { 434 - logger.d('Manually reloading settings for testing...'); 435 - _hasLoadedSettings = false; 436 - await loadSettings(); 437 - } 438 - 439 432 // Helper methods for working with Preferences 440 433 441 434 List<SavedFeed> _getSavedFeedsFromPreferences(Preferences preferences) {
-165
lib/src/features/settings/ui/pages/settings_page.dart
··· 1 - import 'package:atproto/core.dart'; 2 1 import 'package:auto_route/auto_route.dart'; 3 2 import 'package:fluentui_system_icons/fluentui_system_icons.dart'; 4 3 import 'package:flutter/material.dart'; ··· 109 108 } 110 109 } 111 110 112 - Future<void> _handleUpdateSparkPosts() async { 113 - final logger = GetIt.instance<LogService>().getLogger('Settings'); 114 - 115 - try { 116 - // Show loading indicator 117 - showDialog( 118 - context: context, 119 - barrierDismissible: false, 120 - builder: (context) => const Center(child: CircularProgressIndicator()), 121 - ); 122 - 123 - final authRepository = GetIt.instance<AuthRepository>(); 124 - 125 - final did = authRepository.did; 126 - if (did == null || did.isEmpty) { 127 - throw Exception('Not authenticated'); 128 - } 129 - 130 - final atproto = authRepository.atproto; 131 - if (atproto == null) { 132 - throw Exception('AtProto not initialized'); 133 - } 134 - 135 - logger.i('Fetching Spark posts for DID: $did'); 136 - 137 - // Fetch Spark posts directly from atproto to get raw records with URIs 138 - const collection = 'so.sprk.feed.post'; 139 - logger.d('Fetching records from collection: $collection'); 140 - 141 - final result = await atproto.repo.listRecords( 142 - repo: did, 143 - collection: collection, 144 - limit: 100, // Limit can't be more than 100 145 - ); 146 - 147 - final allRecords = result.data.records; 148 - logger.i('Found ${allRecords.length} Spark posts (before filtering)'); 149 - 150 - // Filter out posts with a reply field (only keep top-level posts) 151 - final topLevelRecords = allRecords.where((record) { 152 - final value = record.value; 153 - return !value.containsKey('reply'); 154 - }).toList(); 155 - 156 - logger.i('Filtered to ${topLevelRecords.length} top-level posts'); 157 - 158 - // Filter to only old posts and convert them to new format 159 - final oldPosts = 160 - <({AtUri uri, String? cid, Map<String, dynamic> convertedValue})>[]; 161 - 162 - for (final record in topLevelRecords) { 163 - final value = record.value; 164 - 165 - final isOldPost = 166 - (value.containsKey('text') || value.containsKey('embed')) && 167 - !value.containsKey('caption') && 168 - !value.containsKey('media'); 169 - 170 - if (isOldPost) { 171 - // Convert old post to new format 172 - final converted = Map<String, dynamic>.from(value); 173 - 174 - // Move "text" to "caption": { "text": "..." } 175 - if (converted.containsKey('text')) { 176 - final text = converted.remove('text'); 177 - converted['caption'] = {'text': text}; 178 - } 179 - 180 - // Convert "embed" to "media" and update namespace 181 - if (converted.containsKey('embed')) { 182 - final embed = converted.remove('embed') as Map<String, dynamic>; 183 - final embedType = embed[r'$type'] as String?; 184 - 185 - if (embedType != null) { 186 - // Convert namespace from so.sprk.embed.* to so.sprk.media.* 187 - final newType = embedType.replaceFirst( 188 - 'so.sprk.embed.', 189 - 'so.sprk.media.', 190 - ); 191 - embed[r'$type'] = newType; 192 - } 193 - 194 - converted['media'] = embed; 195 - } 196 - 197 - oldPosts.add(( 198 - uri: record.uri, 199 - cid: record.cid, 200 - convertedValue: converted, 201 - )); 202 - } 203 - } 204 - 205 - logger.i('Found ${oldPosts.length} old posts to convert'); 206 - 207 - var successCount = 0; 208 - var errorCount = 0; 209 - 210 - if (oldPosts.isEmpty) { 211 - logger.i('No old posts found'); 212 - } else { 213 - // Update records in the PDS 214 - for (var i = 0; i < oldPosts.length; i++) { 215 - final post = oldPosts[i]; 216 - try { 217 - // Update the record in the PDS with the converted value 218 - final result = await atproto.repo.putRecord( 219 - repo: did, 220 - collection: collection, 221 - rkey: post.uri.rkey, 222 - record: post.convertedValue, 223 - ); 224 - 225 - successCount++; 226 - logger.i( 227 - 'Updated Post ${i + 1}: ${post.uri}, New CID: ${result.data.cid}', 228 - ); 229 - } catch (e) { 230 - errorCount++; 231 - logger.e('Error updating Post ${i + 1}: ${post.uri}', error: e); 232 - } 233 - } 234 - 235 - logger.i( 236 - 'Update complete: $successCount successful, $errorCount failed', 237 - ); 238 - } 239 - 240 - // Close loading dialog 241 - if (mounted) { 242 - context.router.maybePop(); 243 - } 244 - } catch (e) { 245 - // Close loading dialog if it's open 246 - if (mounted) { 247 - context.router.maybePop(); 248 - } 249 - logger.e('Error fetching Spark records', error: e); 250 - } 251 - } 252 - 253 111 @override 254 112 Widget build(BuildContext context) { 255 113 final l10n = AppLocalizations.of(context); ··· 311 169 trailing: const Icon(FluentIcons.tag_24_regular), 312 170 onTap: () => 313 171 context.router.push(const LabelerManagementRoute()), 314 - contentPadding: const EdgeInsets.symmetric( 315 - horizontal: 16, 316 - vertical: 4, 317 - ), 318 - ), 319 - ), 320 - ), 321 - Padding( 322 - padding: const EdgeInsets.symmetric(vertical: 8), 323 - child: Container( 324 - decoration: BoxDecoration( 325 - color: Theme.of( 326 - context, 327 - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), 328 - borderRadius: BorderRadius.circular(12), 329 - ), 330 - child: ListTile( 331 - title: const Text( 332 - 'Import Legacy Posts', 333 - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 334 - ), 335 - trailing: const Icon(FluentIcons.database_24_regular), 336 - onTap: _handleUpdateSparkPosts, 337 172 contentPadding: const EdgeInsets.symmetric( 338 173 horizontal: 16, 339 174 vertical: 4,