thumbnail_widget.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import 'package:flutter/foundation.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:logging/logging.dart';
  4. import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
  5. import 'package:photos/core/configuration.dart';
  6. import 'package:photos/core/constants.dart';
  7. import 'package:photos/core/errors.dart';
  8. import 'package:photos/core/event_bus.dart';
  9. import 'package:photos/db/files_db.dart';
  10. import 'package:photos/db/trash_db.dart';
  11. import 'package:photos/events/files_updated_event.dart';
  12. import 'package:photos/events/local_photos_updated_event.dart';
  13. import 'package:photos/extensions/string_ext.dart';
  14. import 'package:photos/models/collection.dart';
  15. import 'package:photos/models/file.dart';
  16. import 'package:photos/models/file_type.dart';
  17. import 'package:photos/models/trash_file.dart';
  18. import 'package:photos/services/collections_service.dart';
  19. import 'package:photos/services/favorites_service.dart';
  20. import 'package:photos/ui/viewer/file/file_icons_widget.dart';
  21. import 'package:photos/utils/file_util.dart';
  22. import 'package:photos/utils/thumbnail_util.dart';
  23. class ThumbnailWidget extends StatefulWidget {
  24. final File? file;
  25. final BoxFit fit;
  26. final bool shouldShowSyncStatus;
  27. final bool shouldShowArchiveStatus;
  28. final bool showFavForAlbumOnly;
  29. final bool shouldShowLivePhotoOverlay;
  30. final Duration? diskLoadDeferDuration;
  31. final Duration? serverLoadDeferDuration;
  32. final int thumbnailSize;
  33. final bool shouldShowOwnerAvatar;
  34. ThumbnailWidget(
  35. this.file, {
  36. Key? key,
  37. this.fit = BoxFit.cover,
  38. this.shouldShowSyncStatus = true,
  39. this.shouldShowLivePhotoOverlay = false,
  40. this.shouldShowArchiveStatus = false,
  41. this.showFavForAlbumOnly = false,
  42. this.shouldShowOwnerAvatar = false,
  43. this.diskLoadDeferDuration,
  44. this.serverLoadDeferDuration,
  45. this.thumbnailSize = thumbnailSmallSize,
  46. }) : super(key: key ?? Key(file!.tag));
  47. @override
  48. State<ThumbnailWidget> createState() => _ThumbnailWidgetState();
  49. }
  50. class _ThumbnailWidgetState extends State<ThumbnailWidget> {
  51. static final _logger = Logger("ThumbnailWidget");
  52. bool _hasLoadedThumbnail = false;
  53. bool _isLoadingLocalThumbnail = false;
  54. bool _errorLoadingLocalThumbnail = false;
  55. bool _isLoadingRemoteThumbnail = false;
  56. bool _errorLoadingRemoteThumbnail = false;
  57. ImageProvider? _imageProvider;
  58. @override
  59. void initState() {
  60. super.initState();
  61. }
  62. @override
  63. void dispose() {
  64. super.dispose();
  65. Future.delayed(const Duration(milliseconds: 10), () {
  66. // Cancel request only if the widget has been unmounted
  67. if (!mounted && widget.file!.isRemoteFile && !_hasLoadedThumbnail) {
  68. removePendingGetThumbnailRequestIfAny(widget.file!);
  69. }
  70. });
  71. }
  72. @override
  73. void didUpdateWidget(ThumbnailWidget oldWidget) {
  74. super.didUpdateWidget(oldWidget);
  75. if (widget.file!.generatedID != oldWidget.file!.generatedID) {
  76. _reset();
  77. }
  78. }
  79. @override
  80. Widget build(BuildContext context) {
  81. if (widget.file!.isRemoteFile) {
  82. _loadNetworkImage();
  83. } else {
  84. _loadLocalImage(context);
  85. }
  86. Widget? image;
  87. if (_imageProvider != null) {
  88. image = Image(
  89. image: _imageProvider!,
  90. fit: widget.fit,
  91. );
  92. }
  93. // todo: [2ndJuly22] pref-review if the content Widget which depends on
  94. // thumbnail fetch logic should be part of separate stateFull widget.
  95. // If yes, parent thumbnail widget can be stateless
  96. Widget? content;
  97. if (image != null) {
  98. final List<Widget> contentChildren = [image];
  99. if (FavoritesService.instance.isFavoriteCache(
  100. widget.file!,
  101. checkOnlyAlbum: widget.showFavForAlbumOnly,
  102. )) {
  103. contentChildren.add(const FavoriteOverlayIcon());
  104. }
  105. if (widget.file!.fileType == FileType.video) {
  106. contentChildren.add(const VideoOverlayIcon());
  107. } else if (widget.file!.fileType == FileType.livePhoto &&
  108. widget.shouldShowLivePhotoOverlay) {
  109. contentChildren.add(const LivePhotoOverlayIcon());
  110. }
  111. if (widget.shouldShowOwnerAvatar) {
  112. if (widget.file!.ownerID != null &&
  113. widget.file!.ownerID != Configuration.instance.getUserID()) {
  114. final owner = CollectionsService.instance
  115. .getFileOwner(widget.file!.ownerID!, widget.file!.collectionID);
  116. // hide this icon if the current thumbnail is being showed as album
  117. // cover
  118. contentChildren.add(
  119. OwnerAvatarOverlayIcon(owner),
  120. );
  121. } else if (widget.file!.pubMagicMetadata!.uploaderName != null) {
  122. contentChildren.add(
  123. // Use uploadName hashCode as userID so that different uploader
  124. // get avatar color
  125. OwnerAvatarOverlayIcon(
  126. User(
  127. id: widget.file!.pubMagicMetadata!.uploaderName.sumAsciiValues,
  128. email: '',
  129. name: widget.file!.pubMagicMetadata!.uploaderName,
  130. ),
  131. ),
  132. );
  133. }
  134. }
  135. content = contentChildren.length == 1
  136. ? contentChildren.first
  137. : Stack(
  138. fit: StackFit.expand,
  139. children: contentChildren,
  140. );
  141. }
  142. final List<Widget> viewChildren = [
  143. const ThumbnailPlaceHolder(),
  144. AnimatedOpacity(
  145. opacity: content == null ? 0 : 1.0,
  146. duration: const Duration(milliseconds: 200),
  147. child: content,
  148. )
  149. ];
  150. if (widget.shouldShowSyncStatus && widget.file!.uploadedFileID == null) {
  151. viewChildren.add(const UnSyncedIcon());
  152. }
  153. if (kDebugMode &&
  154. widget.shouldShowSyncStatus &&
  155. widget.file!.uploadedFileID != null) {
  156. if (widget.file!.localID != null) {
  157. viewChildren.add(const DeviceIcon());
  158. } else {
  159. viewChildren.add(const CloudOnlyIcon());
  160. }
  161. }
  162. if (widget.file is TrashFile) {
  163. viewChildren.add(TrashedFileOverlayText(widget.file as TrashFile));
  164. }
  165. // todo: Move this icon overlay to the collection widget.
  166. if (widget.shouldShowArchiveStatus) {
  167. viewChildren.add(const ArchiveOverlayIcon());
  168. }
  169. return Stack(
  170. fit: StackFit.expand,
  171. children: viewChildren,
  172. );
  173. }
  174. void _loadLocalImage(BuildContext context) {
  175. if (!_hasLoadedThumbnail &&
  176. !_errorLoadingLocalThumbnail &&
  177. !_isLoadingLocalThumbnail) {
  178. _isLoadingLocalThumbnail = true;
  179. final cachedSmallThumbnail =
  180. ThumbnailInMemoryLruCache.get(widget.file!, thumbnailSmallSize);
  181. if (cachedSmallThumbnail != null) {
  182. _imageProvider = Image.memory(cachedSmallThumbnail).image;
  183. _hasLoadedThumbnail = true;
  184. } else {
  185. if (widget.diskLoadDeferDuration != null) {
  186. Future.delayed(widget.diskLoadDeferDuration!, () {
  187. if (mounted) {
  188. _getThumbnailFromDisk();
  189. }
  190. });
  191. } else {
  192. _getThumbnailFromDisk();
  193. }
  194. }
  195. }
  196. }
  197. Future _getThumbnailFromDisk() async {
  198. getThumbnailFromLocal(
  199. widget.file!,
  200. size: widget.thumbnailSize,
  201. ).then((thumbData) async {
  202. if (thumbData == null) {
  203. if (widget.file!.uploadedFileID != null) {
  204. _logger.fine("Removing localID reference for " + widget.file!.tag);
  205. widget.file!.localID = null;
  206. if (widget.file is TrashFile) {
  207. TrashDB.instance.update(widget.file as TrashFile);
  208. } else {
  209. FilesDB.instance.update(widget.file!);
  210. }
  211. _loadNetworkImage();
  212. } else {
  213. if (await doesLocalFileExist(widget.file!) == false) {
  214. _logger.info("Deleting file " + widget.file!.tag);
  215. FilesDB.instance.deleteLocalFile(widget.file!);
  216. Bus.instance.fire(
  217. LocalPhotosUpdatedEvent(
  218. [widget.file!],
  219. type: EventType.deletedFromDevice,
  220. source: "thumbFileDeleted",
  221. ),
  222. );
  223. }
  224. }
  225. return;
  226. }
  227. if (mounted) {
  228. final imageProvider = Image.memory(thumbData).image;
  229. _cacheAndRender(imageProvider);
  230. }
  231. ThumbnailInMemoryLruCache.put(
  232. widget.file!,
  233. thumbData,
  234. thumbnailSmallSize,
  235. );
  236. }).catchError((e) {
  237. _logger.warning("Could not load image: ", e);
  238. _errorLoadingLocalThumbnail = true;
  239. });
  240. }
  241. void _loadNetworkImage() {
  242. if (!_hasLoadedThumbnail &&
  243. !_errorLoadingRemoteThumbnail &&
  244. !_isLoadingRemoteThumbnail) {
  245. _isLoadingRemoteThumbnail = true;
  246. final cachedThumbnail = ThumbnailInMemoryLruCache.get(widget.file!);
  247. if (cachedThumbnail != null) {
  248. _imageProvider = Image.memory(cachedThumbnail).image;
  249. _hasLoadedThumbnail = true;
  250. return;
  251. }
  252. if (widget.serverLoadDeferDuration != null) {
  253. Future.delayed(widget.serverLoadDeferDuration!, () {
  254. if (mounted) {
  255. _getThumbnailFromServer();
  256. }
  257. });
  258. } else {
  259. _getThumbnailFromServer();
  260. }
  261. }
  262. }
  263. void _getThumbnailFromServer() async {
  264. try {
  265. final thumbnail = await getThumbnailFromServer(widget.file!);
  266. if (mounted) {
  267. final imageProvider = Image.memory(thumbnail).image;
  268. _cacheAndRender(imageProvider);
  269. }
  270. } catch (e) {
  271. if (e is RequestCancelledError) {
  272. if (mounted) {
  273. _logger.info(
  274. "Thumbnail request was aborted although it is in view, will retry",
  275. );
  276. _reset();
  277. setState(() {});
  278. }
  279. } else {
  280. _logger.severe("Could not load image " + widget.file.toString(), e);
  281. _errorLoadingRemoteThumbnail = true;
  282. }
  283. }
  284. }
  285. void _cacheAndRender(ImageProvider<Object> imageProvider) {
  286. if (imageCache.currentSizeBytes > 256 * 1024 * 1024) {
  287. _logger.info("Clearing image cache");
  288. imageCache.clear();
  289. imageCache.clearLiveImages();
  290. }
  291. precacheImage(imageProvider, context).then((value) {
  292. if (mounted) {
  293. setState(() {
  294. _imageProvider = imageProvider;
  295. _hasLoadedThumbnail = true;
  296. });
  297. }
  298. });
  299. }
  300. void _reset() {
  301. _hasLoadedThumbnail = false;
  302. _isLoadingLocalThumbnail = false;
  303. _isLoadingRemoteThumbnail = false;
  304. _errorLoadingLocalThumbnail = false;
  305. _errorLoadingRemoteThumbnail = false;
  306. _imageProvider = null;
  307. }
  308. }