this repo has no description

feat: Add AppImage widget for optimized image loading and update various screens to use it

+234 -54
+7
ios/Podfile.lock
··· 9 - FlutterMacOS 10 - share_plus (0.0.1): 11 - Flutter 12 - url_launcher_ios (0.0.1): 13 - Flutter 14 ··· 18 - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 19 - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 20 - share_plus (from `.symlinks/plugins/share_plus/ios`) 21 - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 22 23 EXTERNAL SOURCES: ··· 31 :path: ".symlinks/plugins/path_provider_foundation/darwin" 32 share_plus: 33 :path: ".symlinks/plugins/share_plus/ios" 34 url_launcher_ios: 35 :path: ".symlinks/plugins/url_launcher_ios/ios" 36 ··· 40 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 41 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 42 share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f 43 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe 44 45 PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
··· 9 - FlutterMacOS 10 - share_plus (0.0.1): 11 - Flutter 12 + - sqflite_darwin (0.0.4): 13 + - Flutter 14 + - FlutterMacOS 15 - url_launcher_ios (0.0.1): 16 - Flutter 17 ··· 21 - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 22 - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 23 - share_plus (from `.symlinks/plugins/share_plus/ios`) 24 + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) 25 - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 26 27 EXTERNAL SOURCES: ··· 35 :path: ".symlinks/plugins/path_provider_foundation/darwin" 36 share_plus: 37 :path: ".symlinks/plugins/share_plus/ios" 38 + sqflite_darwin: 39 + :path: ".symlinks/plugins/sqflite_darwin/darwin" 40 url_launcher_ios: 41 :path: ".symlinks/plugins/url_launcher_ios/ios" 42 ··· 46 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 47 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 48 share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f 49 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d 50 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe 51 52 PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
+3 -2
lib/screens/comments_page.dart
··· 4 import 'package:grain/models/gallery.dart'; 5 import 'package:grain/utils.dart'; 6 import 'package:grain/widgets/gallery_photo_view.dart'; 7 8 class CommentsPage extends StatefulWidget { 9 final String galleryUri; ··· 232 ), 233 ) 234 : null, 235 - child: Image.network( 236 - comment.focus!.thumb.isNotEmpty 237 ? comment.focus!.thumb 238 : comment.focus!.fullsize, 239 fit: BoxFit.cover,
··· 4 import 'package:grain/models/gallery.dart'; 5 import 'package:grain/utils.dart'; 6 import 'package:grain/widgets/gallery_photo_view.dart'; 7 + import 'package:grain/widgets/app_image.dart'; 8 9 class CommentsPage extends StatefulWidget { 10 final String galleryUri; ··· 233 ), 234 ) 235 : null, 236 + child: AppImage( 237 + url: comment.focus!.thumb.isNotEmpty 238 ? comment.focus!.thumb 239 : comment.focus!.fullsize, 240 fit: BoxFit.cover,
+5 -8
lib/screens/profile_page.dart
··· 2 import 'package:grain/models/gallery.dart'; 3 import 'package:grain/api.dart'; 4 import 'gallery_page.dart'; 5 6 class ProfilePage extends StatefulWidget { 7 final dynamic profile; ··· 304 ), 305 clipBehavior: Clip.antiAlias, 306 child: hasPhoto 307 - ? Image.network( 308 - gallery.items[0].thumb, 309 fit: BoxFit.cover, 310 - width: double.infinity, 311 - height: double.infinity, 312 ) 313 : Center( 314 child: Text( ··· 365 ), 366 clipBehavior: Clip.antiAlias, 367 child: hasPhoto 368 - ? Image.network( 369 - gallery.items[0].thumb, 370 fit: BoxFit.cover, 371 - width: double.infinity, 372 - height: double.infinity, 373 ) 374 : Center( 375 child: Text(
··· 2 import 'package:grain/models/gallery.dart'; 3 import 'package:grain/api.dart'; 4 import 'gallery_page.dart'; 5 + import 'package:grain/widgets/app_image.dart'; 6 7 class ProfilePage extends StatefulWidget { 8 final dynamic profile; ··· 305 ), 306 clipBehavior: Clip.antiAlias, 307 child: hasPhoto 308 + ? AppImage( 309 + url: gallery.items[0].thumb, 310 fit: BoxFit.cover, 311 ) 312 : Center( 313 child: Text( ··· 364 ), 365 clipBehavior: Clip.antiAlias, 366 child: hasPhoto 367 + ? AppImage( 368 + url: gallery.items[0].thumb, 369 fit: BoxFit.cover, 370 ) 371 : Center( 372 child: Text(
+4 -2
lib/screens/splash_page.dart
··· 2 import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; 3 import 'package:grain/app_logger.dart'; 4 import 'package:grain/main.dart'; 5 6 class SplashPage extends StatefulWidget { 7 final void Function(dynamic session)? onSignIn; ··· 55 body: Stack( 56 fit: StackFit.expand, 57 children: [ 58 - Image.network( 59 - 'https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg', 60 fit: BoxFit.cover, 61 ), 62 Container(color: Colors.black.withOpacity(0.4)),
··· 2 import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; 3 import 'package:grain/app_logger.dart'; 4 import 'package:grain/main.dart'; 5 + import 'package:grain/widgets/app_image.dart'; 6 7 class SplashPage extends StatefulWidget { 8 final void Function(dynamic session)? onSignIn; ··· 56 body: Stack( 57 fit: StackFit.expand, 58 children: [ 59 + AppImage( 60 + url: 61 + 'https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg', 62 fit: BoxFit.cover, 63 ), 64 Container(color: Colors.black.withOpacity(0.4)),
+71
lib/widgets/app_image.dart
···
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:cached_network_image/cached_network_image.dart'; 3 + 4 + class AppImage extends StatelessWidget { 5 + final String? url; 6 + final double? width; 7 + final double? height; 8 + final BoxFit fit; 9 + final BorderRadius? borderRadius; 10 + final Widget? placeholder; 11 + final Widget? errorWidget; 12 + 13 + const AppImage({ 14 + super.key, 15 + required this.url, 16 + this.width, 17 + this.height, 18 + this.fit = BoxFit.cover, 19 + this.borderRadius, 20 + this.placeholder, 21 + this.errorWidget, 22 + }); 23 + 24 + @override 25 + Widget build(BuildContext context) { 26 + if (url == null || url!.isEmpty) { 27 + return errorWidget ?? 28 + Container( 29 + width: width, 30 + height: height, 31 + color: Colors.grey[200], 32 + child: const Icon(Icons.broken_image, color: Colors.grey), 33 + ); 34 + } 35 + final image = CachedNetworkImage( 36 + imageUrl: url!, 37 + width: width, 38 + height: height, 39 + fit: fit, 40 + placeholder: (context, _) => 41 + placeholder ?? 42 + Container( 43 + width: width, 44 + height: height, 45 + color: Colors.grey[200], 46 + child: const Center( 47 + child: CircularProgressIndicator( 48 + strokeWidth: 2, 49 + color: Color(0xFF0EA5E9), 50 + ), 51 + ), 52 + ), 53 + errorWidget: (context, _, __) => 54 + errorWidget ?? 55 + Container( 56 + width: width, 57 + height: height, 58 + color: Colors.grey[200], 59 + child: const Icon(Icons.broken_image, color: Colors.grey), 60 + ), 61 + ); 62 + if (borderRadius != null) { 63 + return ClipRRect( 64 + borderRadius: 65 + borderRadius!, // BorderRadius is a subclass of BorderRadiusGeometry 66 + child: image, 67 + ); 68 + } 69 + return image; 70 + } 71 + }
+8 -4
lib/widgets/bottom_nav_bar.dart
··· 1 import 'package:flutter/material.dart'; 2 import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 3 4 class BottomNavBar extends StatelessWidget { 5 final int navIndex; ··· 118 ), 119 ) 120 : null, 121 - child: CircleAvatar( 122 - radius: 12, 123 - backgroundImage: NetworkImage(avatarUrl!), 124 - backgroundColor: Colors.transparent, 125 ), 126 ) 127 : FaIcon(
··· 1 import 'package:flutter/material.dart'; 2 import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 3 + import 'package:grain/widgets/app_image.dart'; 4 5 class BottomNavBar extends StatelessWidget { 6 final int navIndex; ··· 119 ), 120 ) 121 : null, 122 + child: ClipOval( 123 + child: AppImage( 124 + url: avatarUrl!, 125 + width: 24, 126 + height: 24, 127 + fit: BoxFit.cover, 128 + ), 129 ), 130 ) 131 : FaIcon(
+16 -12
lib/widgets/gallery_photo_view.dart
··· 1 import 'package:flutter/material.dart'; 2 import 'package:grain/models/gallery.dart'; 3 4 class GalleryPhotoView extends StatefulWidget { 5 final List<GalleryPhoto> photos; ··· 53 itemCount: widget.photos.length, 54 onPageChanged: (i) => setState(() => _currentIndex = i), 55 itemBuilder: (context, i) => Center( 56 - child: Image.network( 57 - widget.photos[i].fullsize, 58 fit: BoxFit.contain, 59 - loadingBuilder: (context, child, loadingProgress) { 60 - if (loadingProgress == null) return child; 61 - return const Center( 62 child: CircularProgressIndicator( 63 - color: Colors.white, 64 ), 65 - ); 66 - }, 67 - errorBuilder: (context, error, stackTrace) => const Icon( 68 - Icons.broken_image, 69 - color: Colors.white, 70 - size: 64, 71 ), 72 ), 73 ),
··· 1 import 'package:flutter/material.dart'; 2 import 'package:grain/models/gallery.dart'; 3 + import 'package:grain/widgets/app_image.dart'; 4 5 class GalleryPhotoView extends StatefulWidget { 6 final List<GalleryPhoto> photos; ··· 54 itemCount: widget.photos.length, 55 onPageChanged: (i) => setState(() => _currentIndex = i), 56 itemBuilder: (context, i) => Center( 57 + child: AppImage( 58 + url: widget.photos[i].fullsize, 59 fit: BoxFit.contain, 60 + placeholder: Container( 61 + color: Colors.black, 62 + child: const Center( 63 child: CircularProgressIndicator( 64 + strokeWidth: 2, 65 + color: Color(0xFF0EA5E9), 66 ), 67 + ), 68 + ), 69 + errorWidget: Container( 70 + color: Colors.black, 71 + child: const Icon( 72 + Icons.broken_image, 73 + color: Colors.grey, 74 + ), 75 ), 76 ), 77 ),
+7 -6
lib/widgets/gallery_preview.dart
··· 1 import 'package:flutter/material.dart'; 2 import 'package:grain/models/gallery.dart'; 3 4 class GalleryPreview extends StatelessWidget { 5 final Gallery gallery; ··· 18 Expanded( 19 flex: 2, 20 child: photos.isNotEmpty 21 - ? Image.network( 22 - photos[0].thumb, 23 fit: BoxFit.cover, 24 width: double.infinity, 25 height: double.infinity, ··· 33 children: [ 34 Expanded( 35 child: photos.length > 1 36 - ? Image.network( 37 - photos[1].thumb, 38 fit: BoxFit.cover, 39 width: double.infinity, 40 height: double.infinity, ··· 44 const SizedBox(height: 2), 45 Expanded( 46 child: photos.length > 2 47 - ? Image.network( 48 - photos[2].thumb, 49 fit: BoxFit.cover, 50 width: double.infinity, 51 height: double.infinity,
··· 1 import 'package:flutter/material.dart'; 2 import 'package:grain/models/gallery.dart'; 3 + import 'package:grain/widgets/app_image.dart'; 4 5 class GalleryPreview extends StatelessWidget { 6 final Gallery gallery; ··· 19 Expanded( 20 flex: 2, 21 child: photos.isNotEmpty 22 + ? AppImage( 23 + url: photos[0].thumb, 24 fit: BoxFit.cover, 25 width: double.infinity, 26 height: double.infinity, ··· 34 children: [ 35 Expanded( 36 child: photos.length > 1 37 + ? AppImage( 38 + url: photos[1].thumb, 39 fit: BoxFit.cover, 40 width: double.infinity, 41 height: double.infinity, ··· 45 const SizedBox(height: 2), 46 Expanded( 47 child: photos.length > 2 48 + ? AppImage( 49 + url: photos[2].thumb, 50 fit: BoxFit.cover, 51 width: double.infinity, 52 height: double.infinity,
+12 -8
lib/widgets/timeline_item.dart
··· 7 import '../screens/profile_page.dart'; 8 import 'package:grain/api.dart'; 9 import 'package:grain/utils.dart'; 10 11 class TimelineItemWidget extends StatelessWidget { 12 final Gallery gallery; ··· 43 }, 44 child: CircleAvatar( 45 radius: 18, 46 - backgroundImage: 47 - actor?.avatar != null && actor!.avatar.isNotEmpty 48 - ? NetworkImage(actor.avatar) 49 - : null, 50 backgroundColor: Colors.transparent, 51 - child: (actor == null || actor.avatar.isEmpty) 52 - ? const Icon( 53 Icons.account_circle, 54 size: 24, 55 color: Colors.grey, 56 - ) 57 - : null, 58 ), 59 ), 60 const SizedBox(width: 10),
··· 7 import '../screens/profile_page.dart'; 8 import 'package:grain/api.dart'; 9 import 'package:grain/utils.dart'; 10 + import 'package:grain/widgets/app_image.dart'; 11 12 class TimelineItemWidget extends StatelessWidget { 13 final Gallery gallery; ··· 44 }, 45 child: CircleAvatar( 46 radius: 18, 47 backgroundColor: Colors.transparent, 48 + child: (actor != null && actor.avatar.isNotEmpty) 49 + ? ClipOval( 50 + child: AppImage( 51 + url: actor.avatar, 52 + width: 36, 53 + height: 36, 54 + fit: BoxFit.cover, 55 + ), 56 + ) 57 + : const Icon( 58 Icons.account_circle, 59 size: 24, 60 color: Colors.grey, 61 + ), 62 ), 63 ), 64 const SizedBox(width: 10),
+2
macos/Flutter/GeneratedPluginRegistrant.swift
··· 10 import package_info_plus 11 import path_provider_foundation 12 import share_plus 13 import url_launcher_macos 14 import window_to_front 15 ··· 19 FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 20 PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 21 SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 22 UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 23 WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) 24 }
··· 10 import package_info_plus 11 import path_provider_foundation 12 import share_plus 13 + import sqflite_darwin 14 import url_launcher_macos 15 import window_to_front 16 ··· 20 FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 21 PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 22 SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 23 + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 24 UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 25 WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) 26 }
+96
pubspec.lock
··· 33 url: "https://pub.dev" 34 source: hosted 35 version: "2.1.2" 36 characters: 37 dependency: transitive 38 description: ··· 126 description: flutter 127 source: sdk 128 version: "0.0.0" 129 flutter_dotenv: 130 dependency: "direct main" 131 description: ··· 280 url: "https://pub.dev" 281 source: hosted 282 version: "0.4.1" 283 package_info_plus: 284 dependency: "direct main" 285 description: ··· 368 url: "https://pub.dev" 369 source: hosted 370 version: "2.1.8" 371 share_plus: 372 dependency: "direct main" 373 description: ··· 405 url: "https://pub.dev" 406 source: hosted 407 version: "7.0.0" 408 stack_trace: 409 dependency: transitive 410 description: ··· 429 url: "https://pub.dev" 430 source: hosted 431 version: "1.4.1" 432 term_glyph: 433 dependency: transitive 434 description:
··· 33 url: "https://pub.dev" 34 source: hosted 35 version: "2.1.2" 36 + cached_network_image: 37 + dependency: "direct main" 38 + description: 39 + name: cached_network_image 40 + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" 41 + url: "https://pub.dev" 42 + source: hosted 43 + version: "3.4.1" 44 + cached_network_image_platform_interface: 45 + dependency: transitive 46 + description: 47 + name: cached_network_image_platform_interface 48 + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" 49 + url: "https://pub.dev" 50 + source: hosted 51 + version: "4.1.1" 52 + cached_network_image_web: 53 + dependency: transitive 54 + description: 55 + name: cached_network_image_web 56 + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" 57 + url: "https://pub.dev" 58 + source: hosted 59 + version: "1.3.1" 60 characters: 61 dependency: transitive 62 description: ··· 150 description: flutter 151 source: sdk 152 version: "0.0.0" 153 + flutter_cache_manager: 154 + dependency: transitive 155 + description: 156 + name: flutter_cache_manager 157 + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" 158 + url: "https://pub.dev" 159 + source: hosted 160 + version: "3.4.1" 161 flutter_dotenv: 162 dependency: "direct main" 163 description: ··· 312 url: "https://pub.dev" 313 source: hosted 314 version: "0.4.1" 315 + octo_image: 316 + dependency: transitive 317 + description: 318 + name: octo_image 319 + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" 320 + url: "https://pub.dev" 321 + source: hosted 322 + version: "2.1.0" 323 package_info_plus: 324 dependency: "direct main" 325 description: ··· 408 url: "https://pub.dev" 409 source: hosted 410 version: "2.1.8" 411 + rxdart: 412 + dependency: transitive 413 + description: 414 + name: rxdart 415 + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" 416 + url: "https://pub.dev" 417 + source: hosted 418 + version: "0.28.0" 419 share_plus: 420 dependency: "direct main" 421 description: ··· 453 url: "https://pub.dev" 454 source: hosted 455 version: "7.0.0" 456 + sqflite: 457 + dependency: transitive 458 + description: 459 + name: sqflite 460 + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 461 + url: "https://pub.dev" 462 + source: hosted 463 + version: "2.4.2" 464 + sqflite_android: 465 + dependency: transitive 466 + description: 467 + name: sqflite_android 468 + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" 469 + url: "https://pub.dev" 470 + source: hosted 471 + version: "2.4.1" 472 + sqflite_common: 473 + dependency: transitive 474 + description: 475 + name: sqflite_common 476 + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" 477 + url: "https://pub.dev" 478 + source: hosted 479 + version: "2.5.5" 480 + sqflite_darwin: 481 + dependency: transitive 482 + description: 483 + name: sqflite_darwin 484 + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" 485 + url: "https://pub.dev" 486 + source: hosted 487 + version: "2.4.2" 488 + sqflite_platform_interface: 489 + dependency: transitive 490 + description: 491 + name: sqflite_platform_interface 492 + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" 493 + url: "https://pub.dev" 494 + source: hosted 495 + version: "2.4.0" 496 stack_trace: 497 dependency: transitive 498 description: ··· 517 url: "https://pub.dev" 518 source: hosted 519 version: "1.4.1" 520 + synchronized: 521 + dependency: transitive 522 + description: 523 + name: synchronized 524 + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 525 + url: "https://pub.dev" 526 + source: hosted 527 + version: "3.4.0" 528 term_glyph: 529 dependency: transitive 530 description:
+1
pubspec.yaml
··· 44 font_awesome_flutter: ^10.8.0 45 at_uri: ^0.4.0 46 share_plus: ^11.0.0 47 48 dev_dependencies: 49 flutter_test:
··· 44 font_awesome_flutter: ^10.8.0 45 at_uri: ^0.4.0 46 share_plus: ^11.0.0 47 + cached_network_image: ^3.4.1 48 49 dev_dependencies: 50 flutter_test: