thumbnail_widget.dart 8.1 KB

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