···99 Future<ProfileViewDetailed> getProfile(String did);
10101111 /// Get multiple profiles by their DIDs
1212- ///
1212+ ///
1313 /// [dids] A list of DIDs to fetch profiles for
1414 Future<List<ProfileViewDetailed>> getProfiles(List<String> dids);
1515
···11-import 'package:sparksocial/src/features/auth/auth.dart';
21import 'package:sparksocial/src/core/network/atproto/data/repositories/actor_repository.dart';
33-import 'package:sparksocial/src/core/network/atproto/data/repositories/repo_repository.dart';
42import 'package:sparksocial/src/core/network/atproto/data/repositories/feed_repository.dart';
53import 'package:sparksocial/src/core/network/atproto/data/repositories/graph_repository.dart';
64import 'package:sparksocial/src/core/network/atproto/data/repositories/labeler_repository.dart';
55+import 'package:sparksocial/src/core/network/atproto/data/repositories/repo_repository.dart';
66+import 'package:sparksocial/src/features/auth/auth.dart';
7788// All possible endpoints for the Sprk API should be in this contract
99// The implementation should be in each feature's repository
···1111 Future<File?> getCachedFile(String url);
12121313 /// Store a file in the cache with the given key
1414- Future<void> putFile(String url, Uint8List fileBytes);
1414+ Future<void> putFile(String url, Uint8List fileBytes);
15151616 /// Remove a specific file from cache
1717 Future<void> removeFile(String url);
···11import 'package:cached_network_image/cached_network_image.dart';
22+import 'package:collection/collection.dart';
23import 'package:get_it/get_it.dart';
34import 'package:pool/pool.dart';
45import 'package:sparksocial/src/core/network/atproto/data/models/feed_models.dart';
···89import 'package:sparksocial/src/core/storage/storage.dart';
910import 'package:sparksocial/src/core/utils/logging/logging.dart';
1011import 'package:sparksocial/src/features/feed/providers/feed_state.dart';
1111-import 'package:collection/collection.dart';
12121313class DownloadManagerImpl implements DownloadManagerInterface {
1414 DownloadManagerImpl() : _pool = Pool(FeedState.poolSize) {
···7575 final newTasks = <DownloadTask>[];
76767777 while (_tasks.isNotEmpty) {
7878- var task = _tasks.removeFirst();
7878+ final task = _tasks.removeFirst();
7979 if (task.status == DownloadTaskStatus.pending) {
8080 // Try to acquire a pool resource. If pool is full, withResource will wait.
8181 // We want to submit to the pool and let it manage, not block _processQueue.
···8383 // that would make _processQueue sequential for task submission to pool.
8484 // Instead, we launch it and let the pool handle concurrency.
85858686- _pool
8686+ await _pool
8787 .withResource(() => _executeTask(task))
8888 .then((_) {
8989 // This 'then' block executes after _executeTask is fully done
···9393 .catchError((e, s) {
9494 // This catchError is for unexpected errors from _pool.withResource itself,
9595 // or if _executeTask re-throws an error not caught internally.
9696- _logger.e('Error from pool for task ${task.uri}: $e', error: e, stackTrace: s);
9696+ _logger.e('Error from pool for task ${task.uri}: $e', error: e, stackTrace: s as StackTrace);
9797 // Ensure task is marked as failed and removed if not already
9898 if (task.status != DownloadTaskStatus.failed && task.status != DownloadTaskStatus.completed) {
9999 task.status = DownloadTaskStatus.failed;
···163163 throw Exception('Video file was not properly cached after download: ${task.post.videoUrl}');
164164 }
165165 _logger.d('Video file successfully cached: ${task.post.videoUrl}');
166166- break;
167166 case EmbedViewImage():
168168- for (String url in task.post.imageUrls) {
167167+ for (final url in task.post.imageUrls) {
169168 // Download the image and verify it's cached
170169 final fileInfo = await CachedNetworkImageProvider.defaultCacheManager.downloadFile(url, key: url);
171170 if (fileInfo.statusCode != 200) {
172171 _logger.w('Image file was not properly cached after download: $url');
173172 }
174173 }
175175- break;
176174 case EmbedViewBskyRecordWithMedia(:final media):
177175 // Handle nested media in record with media embeds
178176 switch (media) {
···190188 throw Exception('Video file was not properly cached after download: ${task.post.videoUrl}');
191189 }
192190 _logger.d('Video file successfully cached: ${task.post.videoUrl}');
193193- break;
194191 case EmbedViewImage() || EmbedViewBskyImages():
195195- for (String url in task.post.imageUrls) {
192192+ for (final url in task.post.imageUrls) {
196193 // Download the image and verify it's cached
197194 final fileInfo = await CachedNetworkImageProvider.defaultCacheManager.downloadFile(url, key: url);
198195 if (fileInfo.statusCode != 200) {
199196 _logger.w('Image file was not properly cached after download: $url');
200197 }
201198 }
202202- break;
203199 case _:
204200 throw Exception('Unsupported media type: ${media.runtimeType}');
205201 }
206206- break;
207202 case _:
208203 throw Exception('Unsupported media type: ${task.post.embed.runtimeType}');
209204 }
···5050}
51515252abstract class DownloadManagerInterface {
5353-5453 /// Sets the currently active feed. Tasks related to the active feed
5554 /// may be prioritized.
5655 ///
+38-39
lib/src/core/storage/cache/sql_cache_impl.dart
···22import 'dart:convert';
3344import 'package:atproto/atproto.dart';
55+import 'package:atproto_core/atproto_core.dart';
56import 'package:get_it/get_it.dart';
67import 'package:path/path.dart';
88+import 'package:sparksocial/src/core/network/atproto/data/models/models.dart';
79import 'package:sparksocial/src/core/storage/cache/sql_cache_interface.dart';
810import 'package:sparksocial/src/core/storage/storage.dart';
911import 'package:sparksocial/src/core/utils/logging/logging.dart';
1012import 'package:sqflite/sqflite.dart';
1111-1212-import 'package:sparksocial/src/core/network/atproto/data/models/models.dart';
1313-import 'package:atproto_core/atproto_core.dart';
14131514// --- Post Table ---
1615const String _tablePosts = 'cached_posts';
···4342const String _columnAssociationOrder = 'association_order'; // INTEGER, for ordering posts within a feed
44434544class SQLCacheImpl implements SQLCacheInterface {
4646- static Database? _database;
4747- late final SparkLogger _logger;
4848-4945 SQLCacheImpl() {
5046 _logger = GetIt.instance<LogService>().getLogger('SQLCacheImpl');
5147 }
4848+ static Database? _database;
4949+ late final SparkLogger _logger;
52505351 Future<Database> get database async {
5452 if (_database != null) return _database!;
···5755 }
58565957 Future<Database> _initDB() async {
6060- String path = join(await getDatabasesPath(), 'sparksocial_sql_cache.db');
6161- return await openDatabase(
5858+ final path = join(await getDatabasesPath(), 'sparksocial_sql_cache.db');
5959+ return openDatabase(
6260 path,
6361 version: 2, // Increment this if you change the schema
6462 onCreate: _onCreate,
···6765 }
68666967 Future<void> _onCreate(Database db, int version) async {
7070- Batch batch = db.batch();
6868+ final batch = db.batch();
7169 // Posts Table
7270 batch.execute('''
7371 CREATE TABLE $_tablePosts (
···194192 whereArgs: uris.map((uri) => uri.toString()).toList(),
195193 );
196194197197- return maps.map((map) => _mapToPostView(map)).toList();
195195+ return maps.map(_mapToPostView).toList();
198196 }
199197200198 /// Given a list of AtUris, returns a sub-list containing only those URIs
···226224 limit: limit,
227225 offset: offset,
228226 );
229229- return maps.map((map) => _mapToPostView(map)).toList();
227227+ return maps.map(_mapToPostView).toList();
230228 }
231229232230 /// Converts a Map from SQLite back to a PostView.
···235233 return PostView(
236234 uri: AtUri.parse(map[_columnUri] as String),
237235 cid: map[_columnString] as String,
238238- author: ProfileViewBasic.fromJson(jsonDecode(map[_columnAuthor] as String)),
239239- record: PostRecord.fromJson(jsonDecode(map[_columnRecord] as String)),
236236+ author: ProfileViewBasic.fromJson(jsonDecode(map[_columnAuthor] as String) as Map<String, dynamic>),
237237+ record: PostRecord.fromJson(jsonDecode(map[_columnRecord] as String) as Map<String, dynamic>),
240238 isRepost: (map[_columnIsRepost] as int) == 1,
241239 indexedAt: DateTime.parse(map[_columnIndexedAt] as String),
242240 likeCount: map[_columnLikeCount] as int?,
243241 replyCount: map[_columnReplyCount] as int?,
244242 repostCount: map[_columnRepostCount] as int?,
245243 quoteCount: map[_columnQuoteCount] as int?,
246246- labels:
247247- map[_columnLabels] != null
248248- ? (jsonDecode(map[_columnLabels] as String) as List<dynamic>)
249249- .map((e) => Label.fromJson(e as Map<String, dynamic>))
250250- .toList()
251251- : null,
252252- viewer: map[_columnViewer] != null ? Viewer.fromJson(jsonDecode(map[_columnViewer] as String)) : null,
253253- embed: map[_columnEmbed] != null ? EmbedView.fromJson(jsonDecode(map[_columnEmbed] as String)) : null,
244244+ labels: map[_columnLabels] != null
245245+ ? (jsonDecode(map[_columnLabels] as String) as List<dynamic>)
246246+ .map((e) => Label.fromJson(e as Map<String, dynamic>))
247247+ .toList()
248248+ : null,
249249+ viewer: map[_columnViewer] != null ? Viewer.fromJson(jsonDecode(map[_columnViewer] as String) as Map<String, dynamic>) : null,
250250+ embed: map[_columnEmbed] != null ? EmbedView.fromJson(jsonDecode(map[_columnEmbed] as String) as Map<String, dynamic>) : null,
254251 );
255252 }
256253···301298302299 // 2. Add new associations in order
303300 final batch = txn.batch();
304304- for (int i = 0; i < postUris.length; i++) {
301301+ for (var i = 0; i < postUris.length; i++) {
305302 batch.insert(
306303 _tableFeedPostAssociations,
307304 {
···341338 Future<List<PostView>> getPostsForFeed(Feed feed, {int? limit, int? offset}) async {
342339 final feedIdentifier = feed.identifier;
343340 final db = await database;
344344- List<dynamic> arguments = [feedIdentifier];
345345- String limitClause = '';
341341+ final arguments = <dynamic>[feedIdentifier];
342342+ var limitClause = '';
346343347344 if (limit != null) {
348345 limitClause += ' LIMIT ?';
···359356 arguments.add(offset);
360357 }
361358362362- final String sql = '''
359359+ final sql =
360360+ '''
363361 SELECT p.*
364362 FROM $_tablePosts p
365363 INNER JOIN $_tableFeedPostAssociations fpa ON p.$_columnUri = fpa.$_columnPostUriFK
···369367 ''';
370368371369 final List<Map<String, dynamic>> maps = await db.rawQuery(sql, arguments);
372372- return maps.map((map) => _mapToPostView(map)).toList();
370370+ return maps.map(_mapToPostView).toList();
373371 }
374372375373 /// Retrieves post URIs for a specific feed, ordered by their association order
···377375 ///
378376 /// Does NOT update the `lastAccessed` timestamp of any posts.
379377 ///
380380- /// - [feedIdentifier]: The identifier of the feed.
378378+ /// - [feed]: The identifier of the feed.
381379 /// - [limit]: The maximum number of URIs to retrieve. If null, no limit.
382380 /// - [offset]: The number of URIs to skip before starting to retrieve. Requires [limit] to be set.
383381 @override
384382 Future<List<String>> getUrisForFeed(Feed feed, {int? limit, int? offset}) async {
385383 final feedIdentifier = feed.identifier;
386384 final db = await database;
387387- List<dynamic> arguments = [feedIdentifier];
388388- String limitClause = '';
385385+ final arguments = <dynamic>[feedIdentifier];
386386+ var limitClause = '';
389387390388 if (limit != null) {
391389 limitClause += ' LIMIT ?';
···399397 arguments.add(offset);
400398 }
401399402402- final String sql = '''
400400+ final sql =
401401+ '''
403402 SELECT p.$_columnUri
404403 FROM $_tablePosts p
405404 INNER JOIN $_tableFeedPostAssociations fpa ON p.$_columnUri = fpa.$_columnPostUriFK
···429428 whereArgs: [feedIdentifier],
430429 );
431430432432- int currentMaxOrder = -1; // Start before 0 if feed is empty
431431+ var currentMaxOrder = -1; // Start before 0 if feed is empty
433432 if (maxOrderResult.isNotEmpty && maxOrderResult.first['max_order'] != null) {
434433 currentMaxOrder = maxOrderResult.first['max_order'] as int;
435434 }
436435437436 // 2. Add new associations with incrementing order
438437 final batch = txn.batch();
439439- for (int i = 0; i < postUris.length; i++) {
438438+ for (var i = 0; i < postUris.length; i++) {
440439 batch.insert(_tableFeedPostAssociations, {
441440 _columnFeedIdentifierFK: feedIdentifier,
442441 _columnPostUriFK: postUris[i],
···472471 final db = await database;
473472 final cacheManager = GetIt.instance<CacheManagerInterface>();
474473 final countResult = await db.rawQuery('SELECT COUNT(*) FROM $_tablePosts');
475475- final int currentSize = Sqflite.firstIntValue(countResult) ?? 0;
476476- int deletedCount = 0;
474474+ final currentSize = Sqflite.firstIntValue(countResult) ?? 0;
475475+ var deletedCount = 0;
477476478477 if (currentSize > postsToKeep) {
479479- final int numToDelete = currentSize - postsToKeep;
478478+ final numToDelete = currentSize - postsToKeep;
480479 // Find the URIs of the posts to delete (oldest ones)
481480 final List<Map<String, dynamic>> toDeleteMaps = await db.query(
482481 _tablePosts,
···487486488487 if (toDeleteMaps.isNotEmpty) {
489488 for (final map in toDeleteMaps) {
490490- final String uri = map[_columnUri] as String;
489489+ final uri = map[_columnUri] as String;
491490 await cacheManager.removeFile(uri);
492491 }
493492494494- final List<String> urisToDelete = toDeleteMaps.map((map) => map[_columnUri] as String).toList();
493493+ final urisToDelete = toDeleteMaps.map((map) => map[_columnUri] as String).toList();
495494 final placeholders = List.generate(urisToDelete.length, (index) => '?').join(',');
496495 deletedCount = await db.delete(_tablePosts, where: '$_columnUri IN ($placeholders)', whereArgs: urisToDelete);
497496 }
···514513 whereArgs: [cutoffTimestamp],
515514 );
516515 for (final map in toDeleteMaps) {
517517- final String uri = map[_columnUri];
516516+ final uri = map[_columnUri] as String;
518517 await cacheManager.removeFile(uri);
519518 }
520519521521- return await db.delete(_tablePosts, where: '$_columnLastAccessed < ?', whereArgs: [cutoffTimestamp]);
520520+ return db.delete(_tablePosts, where: '$_columnLastAccessed < ?', whereArgs: [cutoffTimestamp]);
522521 }
523522524523 /// Clears the entire PostView cache from the database.
···11import 'dart:async';
2233+import 'package:atproto_core/atproto_core.dart'; // For AtUri
34// We need models for method signatures
45import 'package:sparksocial/src/core/network/atproto/data/models/models.dart';
55-import 'package:atproto_core/atproto_core.dart'; // For AtUri
6677abstract class SQLCacheInterface {
88 /// Caches a single [PostView]. If it already exists, it's updated.
···7171 /// The new posts are added after the existing posts in the feed's order.
7272 /// It's assumed that the [PostView]s corresponding to `postUris` are already cached.
7373 Future<void> appendPostsToFeed(Feed feed, List<String> postUris);
7474-75747675 /// Clears all associations with a specific [Feed] from the cache.
7776 /// Neither the feed metadata nor the posts are removed, only the associations.
···11import 'dart:convert';
22+23import 'package:flutter_secure_storage/flutter_secure_storage.dart';
44+import 'package:sparksocial/src/core/storage/preferences/local_storage_interface.dart';
35import 'package:synchronized/synchronized.dart';
44-import 'local_storage_interface.dart';
5667/// Implementation of LocalStorageInterface using FlutterSecureStorage
78/// for storing sensitive data like tokens, credentials, etc.
89class SecureStorage implements LocalStorageInterface {
99- final FlutterSecureStorage _secureStorage;
1010- final Lock _lock = Lock();
1111-1210 /// Creates a new SecureStorage instance
1311 /// If no secureStorage is provided, a default one will be created
1412 SecureStorage({FlutterSecureStorage? secureStorage}) : _secureStorage = secureStorage ?? const FlutterSecureStorage();
1313+ final FlutterSecureStorage _secureStorage;
1414+ final Lock _lock = Lock();
15151616 @override
1717 Future<void> setString(String key, String value) async {
···11-import 'local_storage_interface.dart';
22-import 'shared_prefs_storage.dart';
33-import 'secure_storage.dart';
11+import 'package:sparksocial/src/core/storage/preferences/local_storage_interface.dart';
22+import 'package:sparksocial/src/core/storage/preferences/secure_storage.dart';
33+import 'package:sparksocial/src/core/storage/preferences/shared_prefs_storage.dart';
4455/// Storage manager providing centralized access to different storage implementations
66/// This is the one that should be used to store and retrieve data from the app
77class StorageManager {
88+ /// Private constructor
99+ StorageManager._();
810 late final LocalStorageInterface _preferences;
911 late final LocalStorageInterface _secureStorage;
1010-1111- /// Private constructor
1212- StorageManager._();
13121413 /// Singleton instance
1514 static final StorageManager _instance = StorageManager._();
···28272928 /// Access to secure storage for sensitive data
3029 LocalStorageInterface get secure => _secureStorage;
3131-3230}
···11import 'package:flutter/material.dart';
2233/// Application color constants.
44-///
44+///
55/// These colors are used throughout the application to ensure consistency
66/// and maintainability.
77class AppColors {
···1919 static const Color deepPurple = Color(0xFF28232D);
2020 static const Color richPurple = Color(0xFF330072);
2121 static const Color brightPurple = Color(0xFFB20AFF);
2222-2222+2323 // Core colors
2424 static const Color pink = Color(0xFFFF2696); // Main app color for buttons and highlights
2525 static const Color white = Color(0xFFFFFFFF);
···7070 static const Color unselectedIconLight = darkPurple;
7171 static const Color selectedIconDark = white;
7272 static const Color unselectedIconDark = darkPurple;
7373-} 7373+}
···55import 'package:atproto/core.dart';
66import 'package:get_it/get_it.dart';
77import 'package:riverpod_annotation/riverpod_annotation.dart';
88-import 'package:sparksocial/src/core/network/atproto/data/models/feed_models.dart';
98import 'package:sparksocial/src/core/feed_algorithms/hardcoded_feed_algorithm.dart';
99+import 'package:sparksocial/src/core/network/atproto/data/models/feed_models.dart';
1010+import 'package:sparksocial/src/core/network/atproto/data/models/labeler_models.dart';
1011import 'package:sparksocial/src/core/network/atproto/data/repositories/feed_repository.dart';
1112import 'package:sparksocial/src/core/network/atproto/data/repositories/sprk_repository.dart';
1313+import 'package:sparksocial/src/core/storage/cache/cache_manager_interface.dart';
1214import 'package:sparksocial/src/core/storage/cache/download_manager_interface.dart';
1315import 'package:sparksocial/src/core/storage/cache/sql_cache_interface.dart';
1416import 'package:sparksocial/src/core/storage/preferences/settings_repository.dart';
···1618import 'package:sparksocial/src/core/utils/logging/logger.dart';
1719import 'package:sparksocial/src/features/feed/providers/feed_state.dart';
1820import 'package:sparksocial/src/features/settings/providers/settings_provider.dart';
1919-import 'package:sparksocial/src/core/storage/cache/cache_manager_interface.dart';
2020-import 'package:sparksocial/src/core/network/atproto/data/models/labeler_models.dart';
21212222part 'feed_provider.g.dart';
2323···6060 // If we were waiting at the end of the feed and new posts have arrived
6161 if (_isWaitingForFreshPostsAtEnd && next.freshPostCount > 0) {
6262 _logger.d('New posts arrived! Loading...');
6363- Future.microtask(() => load()); // Prevent synchronous execution during state change
6363+ Future.microtask(load); // Prevent synchronous execution during state change
6464 }
65656666 // Update preserved state whenever state changes
6767 _preservedState = next;
6868 });
69697070- var isActive = ref.watch(settingsProvider).activeFeed == feed;
7070+ final isActive = ref.watch(settingsProvider).activeFeed == feed;
71717272 // If this notifier has been built before and we have preserved state, use it
7373 if (_hasBeenBuilt && _preservedState != null) {
···101101 bool _isInitialized() {
102102 try {
103103 // Try to access one of the late final fields
104104+ // ignore: unnecessary_statements
104105 _feedRepository;
105106 return true;
106107 } catch (e) {
···148149149150 // gets ONLY the first f cached posts from the database (not all)
150151 final uriStrings = await _sqlCache.getUrisForFeed(_feed, limit: FeedState.firstLoadLimit);
151151- final uris = uriStrings.map((e) => AtUri.parse(e)).toList();
152152- List<Label> labels = <Label>[];
152152+ final uris = uriStrings.map(AtUri.parse).toList();
153153+ final labels = <Label>[];
153154154155 // adds the initial uris to the list of initial uris so that they are not fetched again
155156 _initialUris.addAll(uris);
···158159 if (uris.isNotEmpty) {
159160 // Get existing cached posts to preserve viewer information (like status)
160161 final cachedPosts = await _sqlCache.getPostsByUris(uris);
161161- final cachedPostsMap = {for (var post in cachedPosts) post.uri: post};
162162+ final cachedPostsMap = {for (final post in cachedPosts) post.uri: post};
162163163164 // gets the subscribed labels for the posts
164165 final followedLabelers = await _settingsRepository.getFollowedLabelers();
···168169 final updatedPostViews = await _feedRepository.getPosts(uris, bluesky: _shouldUseBlueskyAPI());
169170170171 // Preserve viewer information from cached posts when updating with fresh data
171171- final List<PostView> mergedPosts = [];
172172- for (var freshPost in updatedPostViews) {
172172+ final mergedPosts = <PostView>[];
173173+ for (final freshPost in updatedPostViews) {
173174 final cachedPost = cachedPostsMap[freshPost.uri];
174175 PostView finalPost;
175176···183184 mergedPosts.add(finalPost);
184185 }
185186186186- for (var post in mergedPosts) {
187187+ for (final post in mergedPosts) {
187188 labels.addAll(post.labels ?? []); // labels from the post
188189 if (post.record.selfLabels != null) {
189190 final recordLabels = <Label>[];
190190- for (SelfLabel selfLabel in post.record.selfLabels!) {
191191+ for (final selfLabel in post.record.selfLabels!) {
191192 recordLabels.add(
192193 Label(uri: post.uri.toString(), value: selfLabel.value, src: post.uri.toString(), createdAt: post.indexedAt),
193194 );
···200201 }
201202202203 // Store the cursor from the initial fetch
203203- String? newCursor = state.cursor;
204204- int fetchedCount = 0;
204204+ // String? newCursor = state.cursor;
205205+ var fetchedCount = 0;
205206 // starts fetching and storing new posts
206207 if (!state.isEndOfNetworkFeed) {
207208 final (int count, List<AtUri> fetchedUris, String? cursor) = await fetch();
208208- newCursor = cursor;
209209+ // newCursor = cursor;
209210 fetchedCount = count;
210211 if (count > 0) {
211212 await store(fetchedUris);
···219220 state.extraInfo,
220221 );
221222222222- for (Label newLabel in labels) {
223223+ for (final newLabel in labels) {
223224 final uri = AtUri.parse(newLabel.uri);
224225 extraInfo.update(uri, (value) {
225226 final existingLabels = value.postLabels;
···261262 loadedPosts: filteredUris,
262263 freshPostCount: 0, // Set to 0 as per strategy
263264 extraInfo: extraInfo,
264264- cursor: newCursor, // Store the cursor from fetch
265265+ // cursor: newCursor, // Store the cursor from fetch
265266 loadingFirstLoad: loadingFirstLoad,
266267 );
267268 _isWaitingForFreshPostsAtEnd = state.length <= 1;
···293294 Future<void> store(List<AtUri> uris) async {
294295 _logger.d('Store called with ${uris.length} URIs. Current freshPostCount: ${state.freshPostCount}');
295296 _isCaching = true; // Set caching flag immediately
296296- int updatedPostCount = 0;
297297+ var updatedPostCount = 0;
297298 state = state.copyWith(error: false);
298299 try {
299300 // checks if the posts have already been cached
···304305305306 // Get existing cached posts to preserve viewer information (like status)
306307 final cachedPosts = await _sqlCache.getPostsByUris(existingUris);
307307- final cachedPostsMap = {for (var post in cachedPosts) post.uri: post};
308308+ final cachedPostsMap = {for (final post in cachedPosts) post.uri: post};
308309309310 final posts = await _feedRepository.getPosts(existingUris, bluesky: _shouldUseBlueskyAPI());
310311311312 // Preserve viewer information from cached posts when updating with fresh data
312312- final List<PostView> mergedPosts = [];
313313- for (var freshPost in posts) {
313313+ final mergedPosts = <PostView>[];
314314+ for (final freshPost in posts) {
314315 final cachedPost = cachedPostsMap[freshPost.uri];
315316 PostView finalPost;
316317···353354354355 // gets the subscribed labels for the new posts
355356 final followedLabelers = await _settingsRepository.getFollowedLabelers();
356356- List<Label> newPostLabels = [];
357357+ var newPostLabels = <Label>[];
357358 try {
358359 final (cursor: _, labels: fetchedLabels) = await _feedRepository.getLabels(nonExistingUris, sources: followedLabelers);
359360 newPostLabels = fetchedLabels;
···362363 newPostLabels = [];
363364 }
364365365365- List<PostView> postsWithLabels = [];
366366- for (var post in nonExistingPosts) {
366366+ final postsWithLabels = <PostView>[];
367367+ for (final post in nonExistingPosts) {
367368 newPostLabels.addAll(post.labels ?? []); // labels from the post
368369 if (post.record.selfLabels != null) {
369370 final recordLabels = <Label>[];
370370- for (SelfLabel selfLabel in post.record.selfLabels!) {
371371+ for (final selfLabel in post.record.selfLabels!) {
371372 recordLabels.add(
372373 Label(uri: post.uri.toString(), value: selfLabel.value, src: post.uri.toString(), createdAt: post.indexedAt),
373374 );
···377378 postsWithLabels.add(post.copyWith(labels: newPostLabels));
378379 }
379380380380- int newPostsCached = 0;
381381- int errorCount = 0;
382382- for (PostView post in postsWithLabels) {
381381+ var newPostsCached = 0;
382382+ var errorCount = 0;
383383+ for (final post in postsWithLabels) {
383384 // concurrent execution
384385 _downloadManager.submitTask(
385386 DownloadTask(
···445446446447 // gets the subscribed labels for the posts
447448 final followedLabelers = await _settingsRepository.getFollowedLabelers();
448448- List<Label> labels = [];
449449+ var labels = <Label>[];
449450 try {
450451 final (cursor: _, labels: fetchedLabels) = await _feedRepository.getLabels(uris, sources: followedLabelers);
451452 labels = fetchedLabels;
···456457457458 // Get the post data for the new URIs
458459 final newPosts = posts.where((post) => uris.contains(post.uri)).toList();
459459- for (var post in newPosts) {
460460+ for (final post in newPosts) {
460461 labels.addAll(post.labels ?? []); // labels from the post
461462 if (post.record.selfLabels != null) {
462463 final recordLabels = <Label>[];
463463- for (SelfLabel selfLabel in post.record.selfLabels!) {
464464+ for (final selfLabel in post.record.selfLabels!) {
464465 recordLabels.add(
465466 Label(uri: post.uri.toString(), value: selfLabel.value, src: post.uri.toString(), createdAt: post.indexedAt),
466467 );
···491492 state.extraInfo,
492493 );
493494494494- for (Label newLabel in labels) {
495495+ for (final newLabel in labels) {
495496 final uri = AtUri.parse(newLabel.uri);
496497 extraInfo.update(uri, (value) {
497498 final existingLabels = value.postLabels;
···592593 final updatedPosts = state.loadedPosts.where((e) => e != uri).toList();
593594594595 // Adjust the index if necessary
595595- int newIndex = currentIndex;
596596+ var newIndex = currentIndex;
596597 if (postIndex != -1) {
597598 if (postIndex < currentIndex) {
598599 // Post was deleted before current position, adjust index down
···611612 }
612613 }
613614614614- _logger.d('Removing post ${uri.toString()}, adjusting index from $currentIndex to $newIndex');
615615+ _logger.d('Removing post $uri, adjusting index from $currentIndex to $newIndex');
615616 state = state.copyWith(loadedPosts: updatedPosts, index: newIndex);
616617 }
617618···622623 try {
623624 final labelPreference = await _settingsRepository.getLabelPreference(label.value);
624625 if (labelPreference.setting == Setting.hide || (labelPreference.adultOnly && hideAdultContent)) {
625625- _logger.d('Hiding post ${uri.toString()} due to label: ${label.value}');
626626+ _logger.d('Hiding post $uri due to label: ${label.value}');
626627 return true;
627628 }
628629 } catch (e) {
···11// Provider to track post updates by URI - when a post gets updated, this gets incremented
22import 'package:flutter_riverpod/flutter_riverpod.dart';
3344-final postUpdateProvider = StateProvider.family<int, String>((ref, postUri) => 0);
44+final StateProviderFamily<int, String> postUpdateProvider = StateProvider.family<int, String>((ref, postUri) => 0);
···2233/// A dialog that displays the alt text (image description) for an image.
44class AltTextDialog extends StatelessWidget {
55+ const AltTextDialog({required this.altText, super.key});
56 final String altText;
66-77- const AltTextDialog({super.key, required this.altText});
8798 @override
109 Widget build(BuildContext context) {
···11import 'dart:typed_data';
2233import 'package:atproto/core.dart';
44+import 'package:get_it/get_it.dart';
55+import 'package:image_picker/image_picker.dart';
46import 'package:riverpod_annotation/riverpod_annotation.dart';
77+import 'package:sparksocial/src/core/auth/data/repositories/auth_repository.dart';
58import 'package:sparksocial/src/core/network/atproto/data/models/actor_models.dart';
69import 'package:sparksocial/src/core/network/atproto/data/repositories/actor_repository.dart';
1010+import 'package:sparksocial/src/core/utils/logging/log_service.dart';
711import 'package:sparksocial/src/core/utils/logging/logger.dart';
812import 'package:sparksocial/src/features/profile/providers/edit_profile_state.dart';
99-import 'package:get_it/get_it.dart';
1010-import 'package:sparksocial/src/core/auth/data/repositories/auth_repository.dart';
1111-import 'package:sparksocial/src/core/utils/logging/log_service.dart';
1212-import 'package:image_picker/image_picker.dart';
1313import 'package:sparksocial/src/features/profile/providers/profile_provider.dart';
14141515part 'edit_profile_provider.g.dart';
···9999 // Ensure the 'avatar' field exists and is a Map before converting to Blob.
100100 // If it's null, it means the user had no avatar on the record.
101101 if (recordData['avatar'] is Map<String, dynamic>) {
102102- avatarToSend = Blob.fromJson(recordData['avatar']);
102102+ avatarToSend = Blob.fromJson(recordData['avatar'] as Map<String, dynamic>);
103103 logger.d('Blob avatar: $avatarToSend');
104104 } else {
105105 // This case handles an inconsistency where localAvatar was a string (URL),
···11import 'package:flutter/material.dart';
22+import 'package:flutter/services.dart';
23import 'package:flutter_riverpod/flutter_riverpod.dart';
33-import 'package:sparksocial/src/core/di/service_locator.dart';
44import 'package:sparksocial/src/core/routing/app_router.dart';
55-import 'package:flutter/services.dart';
55+import 'package:sparksocial/src/core/theme/data/models/app_theme.dart';
66+import 'package:sparksocial/src/core/theme/domain/theme_provider.dart';
6777-import 'core/theme/data/models/app_theme.dart';
88-import 'core/theme/domain/theme_provider.dart';
99-1010-/// SprkApp is the root widget of the new architecture.
1111-/// As features are migrated, they will be integrated here.
128class SprkApp extends ConsumerStatefulWidget {
139 const SprkApp({super.key});
1410···3127 // Force dark status bar and navigation bar
3228 SystemChrome.setSystemUIOverlayStyle(AppTheme.darkSystemUiStyle);
33293434- // Watch theme mode from the provider
3530 final themeMode = ref.watch(themeModeProvider);
36313732 return MaterialApp.router(
···4439 );
4540 }
4641}
4747-4848-/// This method configures all dependencies required for the new architecture.
4949-/// It should be called before the app starts.
5050-Future<void> configureDependencies() async {
5151- // Initialize GetIt
5252- await initServiceLocator();
5353-}