immich_image.dart 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import 'package:cached_network_image/cached_network_image.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  5. import 'package:immich_mobile/shared/models/asset.dart';
  6. import 'package:immich_mobile/shared/models/store.dart';
  7. import 'package:immich_mobile/utils/image_url_builder.dart';
  8. import 'package:photo_manager/photo_manager.dart';
  9. import 'package:openapi/api.dart' as api;
  10. /// Renders an Asset using local data if available, else remote data
  11. class ImmichImage extends StatelessWidget {
  12. const ImmichImage(
  13. this.asset, {
  14. this.width,
  15. this.height,
  16. this.fit = BoxFit.cover,
  17. this.useGrayBoxPlaceholder = false,
  18. this.useProgressIndicator = false,
  19. this.type = api.ThumbnailFormat.WEBP,
  20. super.key,
  21. });
  22. final Asset? asset;
  23. final bool useGrayBoxPlaceholder;
  24. final bool useProgressIndicator;
  25. final double? width;
  26. final double? height;
  27. final BoxFit fit;
  28. final api.ThumbnailFormat type;
  29. @override
  30. Widget build(BuildContext context) {
  31. if (this.asset == null) {
  32. return Container(
  33. decoration: BoxDecoration(
  34. color: Theme.of(context).colorScheme.surfaceVariant,
  35. ),
  36. child: SizedBox(
  37. width: width,
  38. height: height,
  39. child: const Center(
  40. child: Icon(Icons.no_photography),
  41. ),
  42. ),
  43. );
  44. }
  45. final Asset asset = this.asset!;
  46. if (useLocal(asset)) {
  47. return Image(
  48. image: localThumbnailProvider(asset),
  49. width: width,
  50. height: height,
  51. fit: fit,
  52. frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
  53. if (wasSynchronouslyLoaded || frame != null) {
  54. return child;
  55. }
  56. // Show loading if desired
  57. return Stack(
  58. children: [
  59. if (useGrayBoxPlaceholder)
  60. SizedBox.square(
  61. dimension: 250,
  62. child: DecoratedBox(
  63. decoration: BoxDecoration(
  64. color: Theme.of(context).colorScheme.surfaceVariant,
  65. ),
  66. ),
  67. ),
  68. if (useProgressIndicator)
  69. const Center(
  70. child: CircularProgressIndicator(),
  71. ),
  72. ],
  73. );
  74. },
  75. errorBuilder: (context, error, stackTrace) {
  76. if (error is PlatformException &&
  77. error.code == "The asset not found!") {
  78. debugPrint(
  79. "Asset ${asset.localId} does not exist anymore on device!",
  80. );
  81. } else {
  82. debugPrint(
  83. "Error getting thumb for assetId=${asset.localId}: $error",
  84. );
  85. }
  86. return Icon(
  87. Icons.image_not_supported_outlined,
  88. color: Theme.of(context).primaryColor,
  89. );
  90. },
  91. );
  92. }
  93. final String? token = Store.get(StoreKey.accessToken);
  94. final String thumbnailRequestUrl = getThumbnailUrl(asset, type: type);
  95. return CachedNetworkImage(
  96. imageUrl: thumbnailRequestUrl,
  97. httpHeaders: {"Authorization": "Bearer $token"},
  98. cacheKey: getThumbnailCacheKey(asset, type: type),
  99. width: width,
  100. height: height,
  101. // keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
  102. // maxHeightDiskCache = null allows to simply store the webp thumbnail
  103. // from the server and use it for all rendered thumbnail sizes
  104. fit: fit,
  105. fadeInDuration: const Duration(milliseconds: 250),
  106. progressIndicatorBuilder: (context, url, downloadProgress) {
  107. // Show loading if desired
  108. return Stack(
  109. children: [
  110. if (useGrayBoxPlaceholder)
  111. SizedBox.square(
  112. dimension: 250,
  113. child: DecoratedBox(
  114. decoration: BoxDecoration(
  115. color: Theme.of(context).colorScheme.surfaceVariant,
  116. ),
  117. ),
  118. ),
  119. if (useProgressIndicator)
  120. Transform.scale(
  121. scale: 2,
  122. child: Center(
  123. child: CircularProgressIndicator.adaptive(
  124. strokeWidth: 1,
  125. value: downloadProgress.progress,
  126. ),
  127. ),
  128. ),
  129. ],
  130. );
  131. },
  132. errorWidget: (context, url, error) {
  133. if (error is HttpExceptionWithStatus &&
  134. error.statusCode >= 400 &&
  135. error.statusCode < 500) {
  136. debugPrint("Evicting thumbnail '$url' from cache: $error");
  137. CachedNetworkImage.evictFromCache(url);
  138. }
  139. return Icon(
  140. Icons.image_not_supported_outlined,
  141. color: Theme.of(context).primaryColor,
  142. );
  143. },
  144. );
  145. }
  146. static AssetEntityImageProvider localThumbnailProvider(Asset asset) =>
  147. AssetEntityImageProvider(
  148. asset.local!,
  149. isOriginal: false,
  150. thumbnailSize: const ThumbnailSize.square(250),
  151. );
  152. static CachedNetworkImageProvider remoteThumbnailProvider(
  153. Asset asset,
  154. api.ThumbnailFormat type,
  155. Map<String, String> authHeader,
  156. ) =>
  157. CachedNetworkImageProvider(
  158. getThumbnailUrl(asset, type: type),
  159. cacheKey: getThumbnailCacheKey(asset, type: type),
  160. headers: authHeader,
  161. );
  162. /// Precaches this asset for instant load the next time it is shown
  163. static Future<void> precacheAsset(
  164. Asset asset,
  165. BuildContext context, {
  166. type = api.ThumbnailFormat.WEBP,
  167. }) {
  168. if (useLocal(asset)) {
  169. // Precache the local image
  170. return precacheImage(localThumbnailProvider(asset), context);
  171. } else {
  172. final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
  173. // Precache the remote image since we are not using local images
  174. return precacheImage(
  175. remoteThumbnailProvider(asset, type, {"Authorization": authToken}),
  176. context,
  177. );
  178. }
  179. }
  180. static bool useLocal(Asset asset) =>
  181. !asset.isRemote ||
  182. asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
  183. }