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

Revert "fix: android image rendering"

This reverts commit 05a975b2ac9a5c3d19afab87cb4fd9f785b2bd53.

+129 -218
+2
android/app/build.gradle.kts
··· 68 68 buildTypes { 69 69 release { 70 70 signingConfig = signingConfigs.getByName("release") 71 + isShrinkResources = false 72 + isMinifyEnabled = false 71 73 } 72 74 } 73 75 }
+6 -5
lib/src/core/design_system/components/molecules/feed_card.dart
··· 8 8 import 'package:spark/src/core/design_system/tokens/typography.dart'; 9 9 import 'package:spark/src/core/l10n/app_localizations.dart'; 10 10 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 11 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 12 11 13 12 class FeedCard extends StatelessWidget { 14 13 const FeedCard({ ··· 56 55 String? get _description => generator?.description; 57 56 58 57 String get _avatarUrl { 59 - return resolveImageUrlObject(generator?.avatar) ?? ''; 58 + if (generator?.avatar != null) { 59 + return generator!.avatar.toString(); 60 + } 61 + return ''; 60 62 } 61 63 62 64 bool get _isPrimaryAction => !isAdded || !isPinned; ··· 246 248 @override 247 249 Widget build(BuildContext context) { 248 250 final isDark = Theme.of(context).brightness == Brightness.dark; 249 - final resolvedImageUrl = resolveImageUrlString(imageUrl); 250 251 251 - if (resolvedImageUrl == null) { 252 + if (imageUrl.isEmpty) { 252 253 return _FallbackAvatar(isDark: isDark); 253 254 } 254 255 ··· 256 257 borderRadius: BorderRadius.circular(8), 257 258 child: CachedNetworkImage( 258 259 fadeInDuration: Duration.zero, 259 - imageUrl: resolvedImageUrl, 260 + imageUrl: imageUrl, 260 261 width: 36, 261 262 height: 36, 262 263 fit: BoxFit.cover,
+3 -10
lib/src/core/design_system/components/molecules/post_tile.dart
··· 7 7 import 'package:spark/src/core/design_system/tokens/colors.dart'; 8 8 import 'package:spark/src/core/design_system/tokens/shapes.dart'; 9 9 import 'package:spark/src/core/design_system/tokens/typography.dart'; 10 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 11 10 12 11 class PostTile extends StatelessWidget { 13 12 final String thumbnailUrl; ··· 32 31 33 32 @override 34 33 Widget build(BuildContext context) { 35 - final resolvedThumbnailUrl = resolveImageUrlString(thumbnailUrl); 36 34 // Squircle shape from design tokens 37 35 final BorderRadiusGeometry radius = BorderRadius.circular( 38 36 AppShapes.squircleRadius, ··· 57 55 child: Stack( 58 56 fit: StackFit.expand, 59 57 children: [ 60 - if (resolvedThumbnailUrl == null) 61 - const ColoredBox( 62 - color: AppColors.grey800, 63 - child: Icon(Icons.broken_image, color: AppColors.grey400), 64 - ) 65 - else if (nsfwBlur) 58 + if (nsfwBlur) 66 59 ImageFiltered( 67 60 imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), 68 61 child: CachedNetworkImage( 69 62 fadeInDuration: Duration.zero, 70 - imageUrl: resolvedThumbnailUrl, 63 + imageUrl: thumbnailUrl, 71 64 fit: BoxFit.cover, 72 65 placeholder: (context, url) => 73 66 const ColoredBox(color: AppColors.grey800), ··· 80 73 else 81 74 CachedNetworkImage( 82 75 fadeInDuration: Duration.zero, 83 - imageUrl: resolvedThumbnailUrl, 76 + imageUrl: thumbnailUrl, 84 77 fit: BoxFit.cover, 85 78 placeholder: (context, url) => 86 79 const ColoredBox(color: AppColors.grey800),
+2 -4
lib/src/core/design_system/components/molecules/profile_avatar.dart
··· 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:spark/src/core/design_system/tokens/gradients.dart'; 5 5 import 'package:spark/src/core/ui/foundation/colors.dart'; 6 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 7 6 8 7 class ProfileAvatar extends StatelessWidget { 9 8 const ProfileAvatar({ ··· 112 111 required bool isDarkMode, 113 112 required double avatarSize, 114 113 }) { 115 - final resolvedAvatarUrl = resolveImageUrlString(avatarUrl); 116 - if (resolvedAvatarUrl != null) { 114 + if (avatarUrl != null && avatarUrl!.isNotEmpty) { 117 115 return ClipOval( 118 116 child: CachedNetworkImage( 119 117 fadeInDuration: Duration.zero, 120 - imageUrl: resolvedAvatarUrl, 118 + imageUrl: avatarUrl!, 121 119 width: avatarSize, 122 120 height: avatarSize, 123 121 fit: BoxFit.cover,
+4 -2
lib/src/core/design_system/components/molecules/settings_feed_card.dart
··· 7 7 import 'package:spark/src/core/design_system/tokens/typography.dart'; 8 8 import 'package:spark/src/core/l10n/app_localizations.dart'; 9 9 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 10 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 11 10 12 11 enum SettingsFeedCardMode { display, edit } 13 12 ··· 61 60 } 62 61 63 62 String get _avatarUrl { 64 - return resolveImageUrlObject(generator?.avatar) ?? ''; 63 + if (generator?.avatar != null) { 64 + return generator!.avatar.toString(); 65 + } 66 + return ''; 65 67 } 66 68 67 69 @override
+2 -4
lib/src/core/design_system/components/molecules/story_circle.dart
··· 4 4 import 'package:spark/src/core/design_system/tokens/gradients.dart'; 5 5 import 'package:spark/src/core/design_system/tokens/typography.dart'; 6 6 import 'package:spark/src/core/ui/foundation/colors.dart'; 7 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 8 7 9 8 enum StoryType { story, live, cf, create } 10 9 ··· 85 84 Widget build(BuildContext context) { 86 85 final hasStoryRing = type != StoryType.create; 87 86 final ringColor = _getRingColor(); 88 - final resolvedImageUrl = resolveImageUrlString(imageUrl); 89 87 90 88 return SizedBox( 91 89 width: _widgetWidth, ··· 113 111 child: Padding( 114 112 padding: const EdgeInsets.all(_ringGap), 115 113 child: ClipOval( 116 - child: resolvedImageUrl != null 114 + child: imageUrl.isNotEmpty 117 115 ? CachedNetworkImage( 118 116 fadeInDuration: Duration.zero, 119 - imageUrl: resolvedImageUrl, 117 + imageUrl: imageUrl, 120 118 width: _imageSize, 121 119 height: _imageSize, 122 120 fit: BoxFit.cover,
+50 -36
lib/src/core/network/atproto/data/models/feed_models.dart
··· 5 5 import 'package:freezed_annotation/freezed_annotation.dart'; 6 6 import 'package:spark/src/core/network/atproto/data/adapters/bsky/feed_adapter.dart'; 7 7 import 'package:spark/src/core/network/atproto/data/models/models.dart'; 8 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 9 8 import 'package:spark/src/core/utils/uri_converter.dart'; 10 9 11 10 part 'feed_models.freezed.dart'; ··· 387 386 388 387 /// Resolves AT Protocol blob URLs to HTTP URLs for display 389 388 String _resolveAtUriToHttpUrl(Uri uri, {bool isFullsize = false}) { 390 - return resolveImageUrlOrEmpty(uri, isFullsize: isFullsize); 389 + final uriString = uri.toString(); 390 + 391 + // If it's already an HTTP URL, return as is 392 + if (uriString.startsWith('http://') || uriString.startsWith('https://')) { 393 + return uriString; 394 + } 395 + 396 + // If it's an AT Protocol blob URL, convert to Bluesky CDN URL 397 + if (uriString.startsWith('at://')) { 398 + // Parse AT URI format: at://did/collection/rkey 399 + final match = RegExp( 400 + r'^at://([^/]+)/([^/]+)/(.+)$', 401 + ).firstMatch(uriString); 402 + if (match != null) { 403 + final did = match.group(1)!; 404 + final collection = match.group(2)!; 405 + final rkey = match.group(3)!; 406 + 407 + // For blob collections, use Bluesky's CDN 408 + if (collection == 'blob') { 409 + if (isFullsize) { 410 + return 'https://cdn.bsky.app/img/feed_fullsize/plain/$did/$rkey@jpeg'; 411 + } else { 412 + return 'https://cdn.bsky.app/img/feed_thumbnail/plain/$did/$rkey@jpeg'; 413 + } 414 + } 415 + } 416 + } 417 + 418 + // Return empty string for unrecognized URI schemes (e.g., file://) 419 + // to prevent invalid URLs from being passed to image loaders 420 + return ''; 391 421 } 392 422 393 423 String get videoUrl { ··· 416 446 417 447 List<String> get imageUrls { 418 448 final mediaToCheck = displayMedia; 419 - final List<String?> urls; 449 + final List<String> urls; 420 450 switch (mediaToCheck) { 421 451 case MediaViewImage(:final image): 422 - urls = [resolveImageUrlObject(image.fullsize, isFullsize: true)]; 452 + urls = [image.fullsize.toString()]; 423 453 case MediaViewImages(:final images): 424 - urls = images 425 - .map((img) => resolveImageUrlObject(img.fullsize, isFullsize: true)) 426 - .toList(); 454 + urls = images.map((img) => img.fullsize.toString()).toList(); 427 455 case MediaViewBskyImages(:final images): 428 456 urls = images 429 457 .map( ··· 434 462 // Handle nested media in record with media 435 463 switch (media) { 436 464 case MediaViewImage(:final image): 437 - urls = [resolveImageUrlObject(image.fullsize, isFullsize: true)]; 465 + urls = [image.fullsize.toString()]; 438 466 case MediaViewImages(:final images): 439 - urls = images 440 - .map( 441 - (img) => 442 - resolveImageUrlObject(img.fullsize, isFullsize: true), 443 - ) 444 - .toList(); 467 + urls = images.map((img) => img.fullsize.toString()).toList(); 445 468 case MediaViewBskyImages(:final images): 446 469 urls = images 447 470 .map( ··· 455 478 case _: 456 479 urls = []; 457 480 } 458 - return urls.whereType<String>().toList(); 481 + // Filter out invalid URLs (must be http/https) 482 + return urls 483 + .where((url) => url.startsWith('http://') || url.startsWith('https://')) 484 + .toList(); 459 485 } 460 486 461 487 String get thumbnailUrl { 462 488 final mediaToCheck = displayMedia; 463 489 switch (mediaToCheck) { 464 490 case MediaViewVideo(:final thumbnail): 465 - return resolveImageUrlOrEmpty(thumbnail); 491 + return thumbnail.toString(); 466 492 case MediaViewBskyVideo(:final thumbnail): 467 493 return _resolveAtUriToHttpUrl(thumbnail); 468 494 case MediaViewImage(:final image): 469 - return resolveImageUrlOrEmpty(image.thumb); 495 + return image.thumb.toString(); 470 496 case MediaViewImages(:final images): 471 - return resolveImageUrlOrEmpty(images.first.thumb); 497 + return images.first.thumb.toString(); 472 498 case MediaViewBskyImages(:final images): 473 499 return _resolveAtUriToHttpUrl(images.first.thumb); 474 500 case MediaViewBskyRecordWithMedia(:final media): 475 501 // Handle nested media in record with media 476 502 switch (media) { 477 503 case MediaViewVideo(:final thumbnail): 478 - return resolveImageUrlOrEmpty(thumbnail); 504 + return thumbnail.toString(); 479 505 case MediaViewBskyVideo(:final thumbnail): 480 506 return _resolveAtUriToHttpUrl(thumbnail); 481 507 case MediaViewImage(:final image): 482 - return resolveImageUrlOrEmpty(image.thumb); 508 + return image.thumb.toString(); 483 509 case MediaViewImages(:final images): 484 - return resolveImageUrlOrEmpty(images.first.thumb); 510 + return images.first.thumb.toString(); 485 511 case MediaViewBskyImages(:final images): 486 512 return _resolveAtUriToHttpUrl(images.first.thumb); 487 513 case _: ··· 822 848 ThreadPostView(:final post) => post.imageUrls, 823 849 ThreadReplyView(:final reply) => switch (reply.hydratedMedia) { 824 850 // Replies/comments only support a single image (EmbedViewMediaImage) 825 - MediaViewImage(:final image) => [ 826 - resolveImageUrlObject(image.fullsize, isFullsize: true), 827 - ].whereType<String>().toList(), 851 + MediaViewImage(:final image) => [image.fullsize.toString()], 828 852 MediaViewImages(:final images) => 829 - images 830 - .map( 831 - (img) => resolveImageUrlOrEmpty(img.fullsize, isFullsize: true), 832 - ) 833 - .where((url) => url.isNotEmpty) 834 - .toList(), 853 + images.map((img) => img.fullsize.toString()).toList(), 835 854 MediaViewBskyImages(:final images) => 836 - images 837 - .map( 838 - (img) => resolveImageUrlOrEmpty(img.fullsize, isFullsize: true), 839 - ) 840 - .where((url) => url.isNotEmpty) 841 - .toList(), 855 + images.map((img) => img.fullsize.toString()).toList(), 842 856 _ => <String>[], 843 857 }, 844 858 };
+4 -4
lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart
··· 2 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 3 import 'package:spark/src/core/l10n/app_localizations.dart'; 4 4 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 5 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 6 5 import 'package:spark/src/features/search/providers/actor_typeahead_provider.dart'; 7 6 import 'package:spark/src/features/search/providers/actor_typeahead_state.dart'; 8 7 ··· 164 163 separatorBuilder: (_, _) => const Divider(color: Color(0x1FFFFFFF)), 165 164 itemBuilder: (context, index) { 166 165 final actor = typeaheadState.results[index]; 167 - final avatarUrl = resolveImageUrlObject(actor.avatar); 168 166 return ListTile( 169 167 contentPadding: EdgeInsets.zero, 170 168 leading: CircleAvatar( 171 169 backgroundColor: const Color(0x1AFFFFFF), 172 - backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl) : null, 173 - child: avatarUrl == null 170 + backgroundImage: actor.avatar != null 171 + ? NetworkImage(actor.avatar.toString()) 172 + : null, 173 + child: actor.avatar == null 174 174 ? const Icon(Icons.person_outline, color: Colors.white) 175 175 : null, 176 176 ),
+1 -2
lib/src/core/storage/cache/download_manager_impl.dart
··· 5 5 import 'package:pool/pool.dart'; 6 6 import 'package:spark/src/core/network/atproto/data/models/models.dart'; 7 7 import 'package:spark/src/core/storage/cache/download_manager_interface.dart'; 8 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 9 8 import 'package:spark/src/core/utils/logging/logging.dart'; 10 9 import 'package:spark/src/features/feed/providers/feed_state.dart'; 11 10 ··· 222 221 task.post.videoUrl, 223 222 placeholder: CachedNetworkImage( 224 223 fadeInDuration: Duration.zero, 225 - imageUrl: resolveImageUrlOrEmpty(thumbnail), 224 + imageUrl: thumbnail.toString(), 226 225 ), 227 226 cacheConfiguration: BetterPlayerCacheConfiguration( 228 227 useCache: true,
+16 -33
lib/src/core/ui/widgets/image_content.dart
··· 3 3 import 'package:fluentui_system_icons/fluentui_system_icons.dart'; 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:spark/src/core/ui/foundation/colors.dart'; 6 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 7 6 import 'package:spark/src/features/feed/ui/widgets/images/image_carousel.dart'; 8 7 9 8 class ImageContent extends StatelessWidget { ··· 52 51 53 52 @override 54 53 Widget build(BuildContext context) { 55 - final resolvedImageUrl = imageUrls.isEmpty 56 - ? null 57 - : resolveImageUrlString(imageUrls.first, isFullsize: true); 58 - 59 54 return GestureDetector( 60 55 onTap: () => _showImageCarousel(context), 61 56 child: ClipRRect( ··· 67 62 child: Stack( 68 63 fit: StackFit.expand, 69 64 children: [ 70 - if (resolvedImageUrl != null) 71 - CachedNetworkImage( 72 - fadeInDuration: Duration.zero, 73 - imageUrl: resolvedImageUrl, 74 - fit: BoxFit.cover, 75 - placeholder: (context, url) => Container( 76 - color: Colors.grey[850]?.withValues(alpha: 128), 77 - child: const Center( 78 - child: SizedBox( 79 - width: 20, 80 - height: 20, 81 - child: CircularProgressIndicator( 82 - strokeWidth: 2, 83 - color: Colors.white54, 84 - ), 65 + CachedNetworkImage( 66 + fadeInDuration: Duration.zero, 67 + imageUrl: imageUrls.first, 68 + fit: BoxFit.cover, 69 + placeholder: (context, url) => Container( 70 + color: Colors.grey[850]?.withValues(alpha: 128), 71 + child: const Center( 72 + child: SizedBox( 73 + width: 20, 74 + height: 20, 75 + child: CircularProgressIndicator( 76 + strokeWidth: 2, 77 + color: Colors.white54, 85 78 ), 86 79 ), 87 80 ), 88 - errorWidget: (context, url, error) => ColoredBox( 89 - color: AppColors.darkPurple.withValues(alpha: 26), 90 - child: const Center( 91 - child: Icon( 92 - FluentIcons.image_off_24_regular, 93 - size: 24, 94 - color: Colors.white70, 95 - ), 96 - ), 97 - ), 98 - ) 99 - else 100 - ColoredBox( 81 + ), 82 + errorWidget: (context, url, error) => ColoredBox( 101 83 color: AppColors.darkPurple.withValues(alpha: 26), 102 84 child: const Center( 103 85 child: Icon( ··· 107 89 ), 108 90 ), 109 91 ), 92 + ), 110 93 111 94 if (imageUrls.length > 1) 112 95 Positioned(
+2 -4
lib/src/core/ui/widgets/user_avatar.dart
··· 1 1 import 'package:cached_network_image/cached_network_image.dart'; 2 2 import 'package:fluentui_system_icons/fluentui_system_icons.dart'; 3 3 import 'package:flutter/material.dart'; 4 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 5 4 6 5 /// A customizable user avatar with fallback options when no image is available 7 6 class UserAvatar extends StatelessWidget { ··· 27 26 Widget build(BuildContext context) { 28 27 final theme = Theme.of(context); 29 28 final colorScheme = theme.colorScheme; 30 - final resolvedImageUrl = resolveImageUrlString(imageUrl); 31 29 32 30 final effectiveBorderColor = borderColor ?? colorScheme.outline; 33 31 final effectiveBackgroundColor = backgroundColor ?? colorScheme.primary; ··· 35 33 fallbackTextColor ?? colorScheme.onPrimary; 36 34 37 35 // If no image URL is provided, show fallback avatar 38 - if (resolvedImageUrl == null) { 36 + if (imageUrl.isEmpty) { 39 37 return Container( 40 38 width: size, 41 39 height: size, ··· 77 75 clipBehavior: Clip.antiAlias, 78 76 child: CachedNetworkImage( 79 77 fadeInDuration: Duration.zero, 80 - imageUrl: resolvedImageUrl, 78 + imageUrl: imageUrl, 81 79 fit: BoxFit.cover, 82 80 placeholder: (context, url) => ColoredBox( 83 81 color: effectiveBackgroundColor,
-49
lib/src/core/utils/image_url_resolver.dart
··· 1 - final RegExp _atUriPattern = RegExp(r'^at://([^/]+)/([^/]+)/(.+)$'); 2 - 3 - String? resolveImageUrlString(String? raw, {bool isFullsize = false}) { 4 - final candidate = raw?.trim(); 5 - if (candidate == null || candidate.isEmpty || candidate == 'null') { 6 - return null; 7 - } 8 - 9 - if (candidate.startsWith('//')) { 10 - return 'https:$candidate'; 11 - } 12 - 13 - final parsed = Uri.tryParse(candidate); 14 - final scheme = parsed?.scheme.toLowerCase(); 15 - if (scheme == 'http' || scheme == 'https') { 16 - return parsed.toString(); 17 - } 18 - 19 - final match = _atUriPattern.firstMatch(candidate); 20 - if (match == null) { 21 - return null; 22 - } 23 - 24 - final did = match.group(1)!; 25 - final collection = match.group(2)!; 26 - final rkey = match.group(3)!; 27 - 28 - if (collection != 'blob') { 29 - return null; 30 - } 31 - 32 - final variant = isFullsize ? 'feed_fullsize' : 'feed_thumbnail'; 33 - return 'https://cdn.bsky.app/img/$variant/plain/$did/$rkey@jpeg'; 34 - } 35 - 36 - String? resolveImageUrlObject(Object? raw, {bool isFullsize = false}) { 37 - final candidate = switch (raw) { 38 - null => null, 39 - String value => value, 40 - Uri value => value.toString(), 41 - _ => raw.toString(), 42 - }; 43 - 44 - return resolveImageUrlString(candidate, isFullsize: isFullsize); 45 - } 46 - 47 - String resolveImageUrlOrEmpty(Object? raw, {bool isFullsize = false}) { 48 - return resolveImageUrlObject(raw, isFullsize: isFullsize) ?? ''; 49 - }
+1 -2
lib/src/features/comments/ui/widgets/comment_item.dart
··· 15 15 import 'package:spark/src/core/ui/widgets/options_panel.dart'; 16 16 import 'package:spark/src/core/ui/widgets/report_dialog.dart'; 17 17 import 'package:spark/src/core/ui/widgets/user_avatar.dart'; 18 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 19 18 import 'package:spark/src/core/utils/utils.dart'; 20 19 import 'package:spark/src/features/comments/providers/comment_provider.dart'; 21 20 import 'package:spark/src/features/comments/providers/comment_state.dart'; ··· 385 384 @override 386 385 Widget build(BuildContext context) { 387 386 return UserAvatar( 388 - imageUrl: resolveImageUrlObject(widget.thread.post.author.avatar) ?? '', 387 + imageUrl: widget.thread.post.author.avatar.toString(), 389 388 username: widget.thread.post.author.handle, 390 389 size: 36, 391 390 );
+1 -3
lib/src/features/feed/ui/widgets/post/post_overlay.dart
··· 4 4 import 'package:spark/src/core/design_system/components/molecules/known_interactions_bar.dart'; 5 5 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 6 6 import 'package:spark/src/core/utils/label_utils.dart'; 7 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 8 7 import 'package:spark/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart'; 9 8 import 'package:spark/src/features/feed/ui/widgets/post/info_bar.dart'; 10 9 import 'package:spark/src/features/settings/providers/preferences_provider.dart'; ··· 125 124 commentCount: '${post.replyCount ?? 0}', 126 125 shareCount: '${post.repostCount ?? 0}', 127 126 isLiked: isLiked, 128 - profileImageUrl: 129 - resolveImageUrlObject(post.author.avatar) ?? '', 127 + profileImageUrl: post.author.avatar.toString(), 130 128 isImage: 131 129 post.media is MediaViewImages || 132 130 post.media is MediaViewBskyImages,
+2 -4
lib/src/features/home/ui/pages/main_page.dart
··· 8 8 import 'package:spark/src/core/notifications/push_notification_service.dart'; 9 9 import 'package:spark/src/core/routing/app_router.dart'; 10 10 import 'package:spark/src/core/ui/theme/data/models/app_theme.dart'; 11 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 12 11 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 13 12 import 'package:spark/src/features/feed/providers/feed_refresh_trigger_provider.dart'; 14 13 import 'package:spark/src/features/home/providers/navigation_provider.dart'; ··· 97 96 final profileAsync = userDid != null 98 97 ? ref.watch(profileProvider(did: userDid)) 99 98 : null; 100 - final userAvatar = resolveImageUrlObject( 101 - profileAsync?.asData?.value.profile?.avatar, 102 - ); 99 + final userAvatar = profileAsync?.asData?.value.profile?.avatar 100 + ?.toString(); 103 101 104 102 final avatarProvider = userAvatar != null && userAvatar.isNotEmpty 105 103 ? CachedNetworkImageProvider(userAvatar)
+2 -3
lib/src/features/messages/ui/pages/messages_page.dart
··· 4 4 import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 5 import 'package:get_it/get_it.dart'; 6 6 import 'package:spark/src/core/design_system/templates/chat_list_page_template.dart'; 7 - import 'package:spark/src/core/l10n/app_localizations.dart'; 8 7 import 'package:spark/src/core/routing/app_router.dart'; 9 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 10 8 import 'package:spark/src/core/utils/logging/logging.dart'; 9 + import 'package:spark/src/core/l10n/app_localizations.dart'; 11 10 import 'package:spark/src/features/messages/providers/conversations_provider.dart'; 12 11 13 12 @RoutePage() ··· 63 62 otherUserDid: profile.did, 64 63 otherUserHandle: profile.handle, 65 64 otherUserDisplayName: profile.displayName, 66 - otherUserAvatar: resolveImageUrlObject(profile.avatar), 65 + otherUserAvatar: profile.avatar.toString(), 67 66 ), 68 67 ); 69 68 },
+11 -12
lib/src/features/notifications/ui/widgets/notification_item.dart
··· 12 12 import 'package:spark/src/core/routing/app_router.dart'; 13 13 import 'package:spark/src/core/ui/foundation/colors.dart'; 14 14 import 'package:spark/src/core/ui/widgets/user_avatar.dart'; 15 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 16 15 import 'package:spark/src/features/messages/ui/pages/chat_page.dart'; 17 16 import 'package:spark/src/features/notifications/models/grouped_notification.dart'; 18 17 ··· 333 332 case 'so.sprk.media.image#view': 334 333 final thumb = media['thumb']; 335 334 if (thumb != null) { 336 - return resolveImageUrlObject(thumb); 335 + return thumb is String ? thumb : thumb.toString(); 337 336 } 338 337 final fullsize = media['fullsize']; 339 338 if (fullsize != null) { 340 - return resolveImageUrlObject(fullsize, isFullsize: true); 339 + return fullsize is String ? fullsize : fullsize.toString(); 341 340 } 342 341 343 342 // Multiple images - get first one ··· 348 347 if (firstImage != null) { 349 348 final thumb = firstImage['thumb']; 350 349 if (thumb != null) { 351 - return resolveImageUrlObject(thumb); 350 + return thumb is String ? thumb : thumb.toString(); 352 351 } 353 352 final fullsize = firstImage['fullsize']; 354 353 if (fullsize != null) { 355 - return resolveImageUrlObject(fullsize, isFullsize: true); 354 + return fullsize is String ? fullsize : fullsize.toString(); 356 355 } 357 356 } 358 357 } ··· 362 361 case 'app.bsky.embed.video#view': 363 362 final thumbnail = media['thumbnail']; 364 363 if (thumbnail != null) { 365 - return resolveImageUrlObject(thumbnail); 364 + return thumbnail is String ? thumbnail : thumbnail.toString(); 366 365 } 367 366 368 367 // Bluesky images ··· 373 372 if (firstImage != null) { 374 373 final thumb = firstImage['thumb']; 375 374 if (thumb != null) { 376 - return resolveImageUrlObject(thumb); 375 + return thumb is String ? thumb : thumb.toString(); 377 376 } 378 377 final fullsize = firstImage['fullsize']; 379 378 if (fullsize != null) { 380 - return resolveImageUrlObject(fullsize, isFullsize: true); 379 + return fullsize is String ? fullsize : fullsize.toString(); 381 380 } 382 381 } 383 382 } ··· 394 393 if (firstImage != null) { 395 394 final thumb = firstImage['thumb']; 396 395 if (thumb != null) { 397 - return resolveImageUrlObject(thumb); 396 + return thumb is String ? thumb : thumb.toString(); 398 397 } 399 398 } 400 399 } 401 400 } else if (nestedType == 'app.bsky.embed.video#view') { 402 401 final thumbnail = nestedMedia['thumbnail']; 403 402 if (thumbnail != null) { 404 - return resolveImageUrlObject(thumbnail); 403 + return thumbnail is String ? thumbnail : thumbnail.toString(); 405 404 } 406 405 } 407 406 } ··· 438 437 if (authors.length == 1) { 439 438 // Single avatar 440 439 final author = authors[0].author; 441 - final avatarUrl = resolveImageUrlObject(author.avatar) ?? ''; 440 + final avatarUrl = author.avatar?.toString() ?? ''; 442 441 final username = author.displayName ?? author.handle; 443 442 final handleHash = author.handle.hashCode; 444 443 ··· 468 467 ...authors.asMap().entries.map((entry) { 469 468 final index = entry.key; 470 469 final author = entry.value.author; 471 - final avatarUrl = resolveImageUrlObject(author.avatar) ?? ''; 470 + final avatarUrl = author.avatar?.toString() ?? ''; 472 471 final username = author.displayName ?? author.handle; 473 472 final handleHash = author.handle.hashCode; 474 473
+3 -5
lib/src/features/posting/ui/widgets/mention_input_field.dart
··· 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 4 import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; 5 5 import 'package:spark/src/core/design_system/tokens/constants.dart'; 6 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 7 6 import 'package:spark/src/core/utils/text_formatter.dart'; 8 7 import 'package:spark/src/features/posting/models/mention.dart'; 9 8 import 'package:spark/src/features/posting/models/mention_controller.dart'; ··· 386 385 itemCount: typeaheadState.results.length, 387 386 itemBuilder: (context, index) { 388 387 final actor = typeaheadState.results[index]; 389 - final avatarUrl = resolveImageUrlObject(actor.avatar); 390 388 return ListTile( 391 389 dense: true, 392 390 leading: CircleAvatar( 393 391 radius: 16, 394 - backgroundImage: avatarUrl != null 395 - ? NetworkImage(avatarUrl) 392 + backgroundImage: actor.avatar != null 393 + ? NetworkImage(actor.avatar.toString()) 396 394 : null, 397 - child: avatarUrl == null 395 + child: actor.avatar == null 398 396 ? const Icon(Icons.person, size: 16) 399 397 : null, 400 398 ),
+4 -4
lib/src/features/search/ui/pages/search_page.dart
··· 9 9 import 'package:spark/src/core/l10n/app_localizations.dart'; 10 10 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 11 11 import 'package:spark/src/core/routing/app_router.dart'; 12 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 13 12 import 'package:spark/src/features/search/providers/actor_typeahead_provider.dart'; 14 13 import 'package:spark/src/features/search/providers/actor_typeahead_state.dart'; 15 14 import 'package:spark/src/features/search/providers/post_search_provider.dart'; ··· 223 222 separatorBuilder: (context, index) => const Divider(height: 1), 224 223 itemBuilder: (context, index) { 225 224 final actor = state.results[index]; 226 - final avatarUrl = resolveImageUrlObject(actor.avatar); 227 225 228 226 return ListTile( 229 227 onTap: () => onSuggestionSelected(actor), 230 228 contentPadding: const EdgeInsets.symmetric(vertical: 4), 231 229 leading: CircleAvatar( 232 230 radius: 18, 233 - backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl) : null, 234 - child: avatarUrl == null ? const Icon(Icons.person) : null, 231 + backgroundImage: actor.avatar != null 232 + ? NetworkImage(actor.avatar.toString()) 233 + : null, 234 + child: actor.avatar == null ? const Icon(Icons.person) : null, 235 235 ), 236 236 title: Text(actor.displayName ?? actor.handle), 237 237 subtitle: Text('@${actor.handle}'),
+1 -2
lib/src/features/settings/ui/pages/labeler_label_settings_page.dart
··· 12 12 import 'package:spark/src/core/network/atproto/data/repositories/actor_repository.dart'; 13 13 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 14 14 import 'package:spark/src/core/l10n/app_localizations.dart'; 15 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 16 15 import 'package:spark/src/core/utils/logging/logging.dart'; 17 16 import 'package:spark/src/features/settings/providers/settings_provider.dart'; 18 17 import 'package:spark/src/features/settings/ui/widgets/widgets.dart'; ··· 571 570 borderRadius: BorderRadius.circular(100), 572 571 child: profile.avatar != null 573 572 ? CachedNetworkImage( 574 - imageUrl: resolveImageUrlOrEmpty(profile.avatar), 573 + imageUrl: profile.avatar!.toString(), 575 574 width: 36, 576 575 height: 36, 577 576 fit: BoxFit.cover,
+1 -2
lib/src/features/settings/ui/pages/labeler_management_page.dart
··· 10 10 import 'package:spark/src/core/network/atproto/data/repositories/actor_repository.dart'; 11 11 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 12 12 import 'package:spark/src/core/routing/app_router.dart'; 13 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 14 13 import 'package:spark/src/core/utils/logging/logging.dart'; 15 14 import 'package:spark/src/features/settings/providers/settings_provider.dart'; 16 15 ··· 399 398 borderRadius: BorderRadius.circular(100), 400 399 child: profile.avatar != null 401 400 ? CachedNetworkImage( 402 - imageUrl: resolveImageUrlOrEmpty(profile.avatar), 401 + imageUrl: profile.avatar!.toString(), 403 402 width: 36, 404 403 height: 36, 405 404 fit: BoxFit.cover,
+6 -14
lib/src/features/stories/ui/pages/author_stories_page.dart
··· 8 8 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 9 9 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 10 10 import 'package:spark/src/core/routing/app_router.dart'; 11 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 12 11 import 'package:spark/src/features/stories/ui/pages/story_page.dart'; 13 12 14 13 @RoutePage() ··· 254 253 return Scaffold(body: Center(child: Text(l10n.emptyNoStories))); 255 254 } 256 255 257 - final avatarUrl = resolveImageUrlObject(widget.author.avatar); 258 - 259 256 return Scaffold( 260 257 backgroundColor: Colors.black, 261 258 body: SafeArea( ··· 364 361 shape: BoxShape.circle, 365 362 ), 366 363 child: ClipOval( 367 - child: avatarUrl != null 368 - ? CachedNetworkImage( 369 - imageUrl: avatarUrl, 370 - fit: BoxFit.cover, 371 - errorWidget: (context, url, error) => 372 - const Icon( 373 - Icons.person, 374 - color: Colors.white, 375 - ), 376 - ) 377 - : const Icon( 364 + child: CachedNetworkImage( 365 + imageUrl: widget.author.avatar.toString(), 366 + fit: BoxFit.cover, 367 + errorWidget: (context, url, error) => 368 + const Icon( 378 369 Icons.person, 379 370 color: Colors.white, 380 371 ), 372 + ), 381 373 ), 382 374 ), 383 375 ),
+3 -8
lib/src/features/stories/ui/pages/story_manager_page.dart
··· 5 5 import 'package:spark/src/core/l10n/app_localizations.dart'; 6 6 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 7 7 import 'package:spark/src/core/routing/app_router.dart'; 8 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 9 8 import 'package:spark/src/features/stories/providers/story_auto_delete_provider.dart'; 10 9 import 'package:spark/src/features/stories/providers/story_manager_provider.dart'; 11 10 ··· 100 99 ? '${age.inMinutes}m' 101 100 : 'now'; 102 101 final thumbUrl = switch (story.media) { 103 - MediaViewVideo(:final thumbnail) => resolveImageUrlOrEmpty( 104 - thumbnail, 105 - ), 106 - MediaViewImage(:final image) => resolveImageUrlOrEmpty( 107 - image.thumb, 108 - ), 109 - _ => resolveImageUrlOrEmpty(story.author.avatar), 102 + MediaViewVideo(:final thumbnail) => thumbnail.toString(), 103 + MediaViewImage(:final image) => image.thumb.toString(), 104 + _ => story.author.avatar.toString(), 110 105 }; 111 106 return Material( 112 107 color: Colors.transparent,
+2 -6
lib/src/features/stories/ui/pages/story_page.dart
··· 8 8 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 9 9 import 'package:spark/src/core/network/atproto/data/models/story_embed_models.dart'; 10 10 import 'package:spark/src/core/routing/app_router.dart'; 11 - import 'package:spark/src/core/utils/image_url_resolver.dart'; 12 11 import 'package:video_player/video_player.dart'; 13 12 14 13 @RoutePage() ··· 145 144 146 145 String _getImageUrl(StoryView story) { 147 146 return switch (story.media) { 148 - MediaViewImage(:final image) => resolveImageUrlOrEmpty( 149 - image.fullsize, 150 - isFullsize: true, 151 - ), 152 - _ => resolveImageUrlOrEmpty(widget.story.author.avatar), 147 + MediaViewImage(:final image) => image.fullsize.toString(), 148 + _ => widget.story.author.avatar.toString(), 153 149 }; 154 150 } 155 151