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 9 - FlutterMacOS 10 10 - share_plus (0.0.1): 11 11 - Flutter 12 + - sqflite_darwin (0.0.4): 13 + - Flutter 14 + - FlutterMacOS 12 15 - url_launcher_ios (0.0.1): 13 16 - Flutter 14 17 ··· 18 21 - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 19 22 - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 20 23 - share_plus (from `.symlinks/plugins/share_plus/ios`) 24 + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) 21 25 - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 22 26 23 27 EXTERNAL SOURCES: ··· 31 35 :path: ".symlinks/plugins/path_provider_foundation/darwin" 32 36 share_plus: 33 37 :path: ".symlinks/plugins/share_plus/ios" 38 + sqflite_darwin: 39 + :path: ".symlinks/plugins/sqflite_darwin/darwin" 34 40 url_launcher_ios: 35 41 :path: ".symlinks/plugins/url_launcher_ios/ios" 36 42 ··· 40 46 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 41 47 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 42 48 share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f 49 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d 43 50 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe 44 51 45 52 PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
+3 -2
lib/screens/comments_page.dart
··· 4 4 import 'package:grain/models/gallery.dart'; 5 5 import 'package:grain/utils.dart'; 6 6 import 'package:grain/widgets/gallery_photo_view.dart'; 7 + import 'package:grain/widgets/app_image.dart'; 7 8 8 9 class CommentsPage extends StatefulWidget { 9 10 final String galleryUri; ··· 232 233 ), 233 234 ) 234 235 : null, 235 - child: Image.network( 236 - comment.focus!.thumb.isNotEmpty 236 + child: AppImage( 237 + url: comment.focus!.thumb.isNotEmpty 237 238 ? comment.focus!.thumb 238 239 : comment.focus!.fullsize, 239 240 fit: BoxFit.cover,
+5 -8
lib/screens/profile_page.dart
··· 2 2 import 'package:grain/models/gallery.dart'; 3 3 import 'package:grain/api.dart'; 4 4 import 'gallery_page.dart'; 5 + import 'package:grain/widgets/app_image.dart'; 5 6 6 7 class ProfilePage extends StatefulWidget { 7 8 final dynamic profile; ··· 304 305 ), 305 306 clipBehavior: Clip.antiAlias, 306 307 child: hasPhoto 307 - ? Image.network( 308 - gallery.items[0].thumb, 308 + ? AppImage( 309 + url: gallery.items[0].thumb, 309 310 fit: BoxFit.cover, 310 - width: double.infinity, 311 - height: double.infinity, 312 311 ) 313 312 : Center( 314 313 child: Text( ··· 365 364 ), 366 365 clipBehavior: Clip.antiAlias, 367 366 child: hasPhoto 368 - ? Image.network( 369 - gallery.items[0].thumb, 367 + ? AppImage( 368 + url: gallery.items[0].thumb, 370 369 fit: BoxFit.cover, 371 - width: double.infinity, 372 - height: double.infinity, 373 370 ) 374 371 : Center( 375 372 child: Text(
+4 -2
lib/screens/splash_page.dart
··· 2 2 import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; 3 3 import 'package:grain/app_logger.dart'; 4 4 import 'package:grain/main.dart'; 5 + import 'package:grain/widgets/app_image.dart'; 5 6 6 7 class SplashPage extends StatefulWidget { 7 8 final void Function(dynamic session)? onSignIn; ··· 55 56 body: Stack( 56 57 fit: StackFit.expand, 57 58 children: [ 58 - Image.network( 59 - 'https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg', 59 + AppImage( 60 + url: 61 + 'https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg', 60 62 fit: BoxFit.cover, 61 63 ), 62 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 1 import 'package:flutter/material.dart'; 2 2 import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 3 + import 'package:grain/widgets/app_image.dart'; 3 4 4 5 class BottomNavBar extends StatelessWidget { 5 6 final int navIndex; ··· 118 119 ), 119 120 ) 120 121 : null, 121 - child: CircleAvatar( 122 - radius: 12, 123 - backgroundImage: NetworkImage(avatarUrl!), 124 - backgroundColor: Colors.transparent, 122 + child: ClipOval( 123 + child: AppImage( 124 + url: avatarUrl!, 125 + width: 24, 126 + height: 24, 127 + fit: BoxFit.cover, 128 + ), 125 129 ), 126 130 ) 127 131 : FaIcon(
+16 -12
lib/widgets/gallery_photo_view.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:grain/models/gallery.dart'; 3 + import 'package:grain/widgets/app_image.dart'; 3 4 4 5 class GalleryPhotoView extends StatefulWidget { 5 6 final List<GalleryPhoto> photos; ··· 53 54 itemCount: widget.photos.length, 54 55 onPageChanged: (i) => setState(() => _currentIndex = i), 55 56 itemBuilder: (context, i) => Center( 56 - child: Image.network( 57 - widget.photos[i].fullsize, 57 + child: AppImage( 58 + url: widget.photos[i].fullsize, 58 59 fit: BoxFit.contain, 59 - loadingBuilder: (context, child, loadingProgress) { 60 - if (loadingProgress == null) return child; 61 - return const Center( 60 + placeholder: Container( 61 + color: Colors.black, 62 + child: const Center( 62 63 child: CircularProgressIndicator( 63 - color: Colors.white, 64 + strokeWidth: 2, 65 + color: Color(0xFF0EA5E9), 64 66 ), 65 - ); 66 - }, 67 - errorBuilder: (context, error, stackTrace) => const Icon( 68 - Icons.broken_image, 69 - color: Colors.white, 70 - size: 64, 67 + ), 68 + ), 69 + errorWidget: Container( 70 + color: Colors.black, 71 + child: const Icon( 72 + Icons.broken_image, 73 + color: Colors.grey, 74 + ), 71 75 ), 72 76 ), 73 77 ),
+7 -6
lib/widgets/gallery_preview.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:grain/models/gallery.dart'; 3 + import 'package:grain/widgets/app_image.dart'; 3 4 4 5 class GalleryPreview extends StatelessWidget { 5 6 final Gallery gallery; ··· 18 19 Expanded( 19 20 flex: 2, 20 21 child: photos.isNotEmpty 21 - ? Image.network( 22 - photos[0].thumb, 22 + ? AppImage( 23 + url: photos[0].thumb, 23 24 fit: BoxFit.cover, 24 25 width: double.infinity, 25 26 height: double.infinity, ··· 33 34 children: [ 34 35 Expanded( 35 36 child: photos.length > 1 36 - ? Image.network( 37 - photos[1].thumb, 37 + ? AppImage( 38 + url: photos[1].thumb, 38 39 fit: BoxFit.cover, 39 40 width: double.infinity, 40 41 height: double.infinity, ··· 44 45 const SizedBox(height: 2), 45 46 Expanded( 46 47 child: photos.length > 2 47 - ? Image.network( 48 - photos[2].thumb, 48 + ? AppImage( 49 + url: photos[2].thumb, 49 50 fit: BoxFit.cover, 50 51 width: double.infinity, 51 52 height: double.infinity,
+12 -8
lib/widgets/timeline_item.dart
··· 7 7 import '../screens/profile_page.dart'; 8 8 import 'package:grain/api.dart'; 9 9 import 'package:grain/utils.dart'; 10 + import 'package:grain/widgets/app_image.dart'; 10 11 11 12 class TimelineItemWidget extends StatelessWidget { 12 13 final Gallery gallery; ··· 43 44 }, 44 45 child: CircleAvatar( 45 46 radius: 18, 46 - backgroundImage: 47 - actor?.avatar != null && actor!.avatar.isNotEmpty 48 - ? NetworkImage(actor.avatar) 49 - : null, 50 47 backgroundColor: Colors.transparent, 51 - child: (actor == null || actor.avatar.isEmpty) 52 - ? const Icon( 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( 53 58 Icons.account_circle, 54 59 size: 24, 55 60 color: Colors.grey, 56 - ) 57 - : null, 61 + ), 58 62 ), 59 63 ), 60 64 const SizedBox(width: 10),
+2
macos/Flutter/GeneratedPluginRegistrant.swift
··· 10 10 import package_info_plus 11 11 import path_provider_foundation 12 12 import share_plus 13 + import sqflite_darwin 13 14 import url_launcher_macos 14 15 import window_to_front 15 16 ··· 19 20 FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 20 21 PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 21 22 SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 23 + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 22 24 UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 23 25 WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) 24 26 }
+96
pubspec.lock
··· 33 33 url: "https://pub.dev" 34 34 source: hosted 35 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" 36 60 characters: 37 61 dependency: transitive 38 62 description: ··· 126 150 description: flutter 127 151 source: sdk 128 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" 129 161 flutter_dotenv: 130 162 dependency: "direct main" 131 163 description: ··· 280 312 url: "https://pub.dev" 281 313 source: hosted 282 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" 283 323 package_info_plus: 284 324 dependency: "direct main" 285 325 description: ··· 368 408 url: "https://pub.dev" 369 409 source: hosted 370 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" 371 419 share_plus: 372 420 dependency: "direct main" 373 421 description: ··· 405 453 url: "https://pub.dev" 406 454 source: hosted 407 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" 408 496 stack_trace: 409 497 dependency: transitive 410 498 description: ··· 429 517 url: "https://pub.dev" 430 518 source: hosted 431 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" 432 528 term_glyph: 433 529 dependency: transitive 434 530 description:
+1
pubspec.yaml
··· 44 44 font_awesome_flutter: ^10.8.0 45 45 at_uri: ^0.4.0 46 46 share_plus: ^11.0.0 47 + cached_network_image: ^3.4.1 47 48 48 49 dev_dependencies: 49 50 flutter_test: