thumbnail_widget.dart 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import 'package:flutter/material.dart';
  2. import 'package:photos/core/cache/thumbnail_cache.dart';
  3. import 'package:photos/core/errors.dart';
  4. import 'package:photos/core/event_bus.dart';
  5. import 'package:photos/db/files_db.dart';
  6. import 'package:photos/events/local_photos_updated_event.dart';
  7. import 'package:photos/models/file.dart';
  8. import 'package:logging/logging.dart';
  9. import 'package:photos/core/constants.dart';
  10. import 'package:photos/models/file_type.dart';
  11. import 'package:photos/ui/common_elements.dart';
  12. import 'package:photos/utils/thumbnail_util.dart';
  13. class ThumbnailWidget extends StatefulWidget {
  14. final File file;
  15. final BoxFit fit;
  16. final bool shouldShowSyncStatus;
  17. final Duration diskLoadDeferDuration;
  18. final Duration serverLoadDeferDuration;
  19. ThumbnailWidget(
  20. this.file, {
  21. Key key,
  22. this.fit = BoxFit.cover,
  23. this.shouldShowSyncStatus = true,
  24. this.diskLoadDeferDuration,
  25. this.serverLoadDeferDuration,
  26. }) : super(key: key ?? Key(file.tag()));
  27. @override
  28. _ThumbnailWidgetState createState() => _ThumbnailWidgetState();
  29. }
  30. class _ThumbnailWidgetState extends State<ThumbnailWidget> {
  31. static final _logger = Logger("ThumbnailWidget");
  32. static final kVideoIconOverlay = Container(
  33. height: 64,
  34. child: Icon(
  35. Icons.play_circle_outline,
  36. size: 40,
  37. color: Colors.white70,
  38. ),
  39. );
  40. static final kUnsyncedIconOverlay = Container(
  41. decoration: BoxDecoration(
  42. gradient: LinearGradient(
  43. begin: Alignment.topCenter,
  44. end: Alignment.bottomCenter,
  45. colors: [
  46. Colors.transparent,
  47. Colors.black.withOpacity(0.6),
  48. ],
  49. stops: [0.75, 1],
  50. ),
  51. ),
  52. child: Align(
  53. alignment: Alignment.bottomRight,
  54. child: Padding(
  55. padding: const EdgeInsets.only(right: 8, bottom: 4),
  56. child: Icon(
  57. Icons.cloud_off_outlined,
  58. size: 18,
  59. color: Colors.white.withOpacity(0.9),
  60. ),
  61. ),
  62. ),
  63. );
  64. static final Widget loadingWidget = Container(
  65. alignment: Alignment.center,
  66. color: Colors.grey[900],
  67. );
  68. bool _hasLoadedThumbnail = false;
  69. bool _isLoadingThumbnail = false;
  70. bool _encounteredErrorLoadingThumbnail = false;
  71. ImageProvider _imageProvider;
  72. @override
  73. void initState() {
  74. super.initState();
  75. }
  76. @override
  77. void dispose() {
  78. super.dispose();
  79. Future.delayed(Duration.zero, () {
  80. // Cancel request only if the widget has been unmounted
  81. if (!mounted && widget.file.localID == null && !_hasLoadedThumbnail) {
  82. removePendingGetThumbnailRequestIfAny(widget.file);
  83. }
  84. });
  85. }
  86. @override
  87. void didUpdateWidget(ThumbnailWidget oldWidget) {
  88. super.didUpdateWidget(oldWidget);
  89. if (widget.file.generatedID != oldWidget.file.generatedID) {
  90. _reset();
  91. }
  92. }
  93. @override
  94. Widget build(BuildContext context) {
  95. if (widget.file.localID == null) {
  96. _loadNetworkImage();
  97. } else {
  98. _loadLocalImage(context);
  99. }
  100. var image;
  101. if (_imageProvider != null) {
  102. image = Image(
  103. image: _imageProvider,
  104. fit: widget.fit,
  105. );
  106. }
  107. var content;
  108. if (image != null) {
  109. if (widget.file.fileType == FileType.video) {
  110. content = Stack(
  111. children: [
  112. image,
  113. kVideoIconOverlay,
  114. ],
  115. fit: StackFit.expand,
  116. );
  117. } else {
  118. content = image;
  119. }
  120. }
  121. return Stack(
  122. children: [
  123. loadingWidget,
  124. AnimatedOpacity(
  125. opacity: content == null ? 0 : 1.0,
  126. duration: Duration(milliseconds: 200),
  127. child: content,
  128. ),
  129. widget.shouldShowSyncStatus && widget.file.uploadedFileID == null
  130. ? kUnsyncedIconOverlay
  131. : emptyContainer,
  132. ],
  133. fit: StackFit.expand,
  134. );
  135. }
  136. void _loadLocalImage(BuildContext context) {
  137. if (!_hasLoadedThumbnail &&
  138. !_encounteredErrorLoadingThumbnail &&
  139. !_isLoadingThumbnail) {
  140. _isLoadingThumbnail = true;
  141. final cachedSmallThumbnail =
  142. ThumbnailLruCache.get(widget.file, THUMBNAIL_SMALL_SIZE);
  143. if (cachedSmallThumbnail != null) {
  144. _imageProvider = Image.memory(cachedSmallThumbnail).image;
  145. _hasLoadedThumbnail = true;
  146. } else {
  147. if (widget.diskLoadDeferDuration != null) {
  148. Future.delayed(widget.diskLoadDeferDuration, () {
  149. if (mounted) {
  150. _getThumbnailFromDisk();
  151. }
  152. });
  153. } else {
  154. _getThumbnailFromDisk();
  155. }
  156. }
  157. }
  158. }
  159. Future _getThumbnailFromDisk() async {
  160. widget.file.getAsset().then((asset) async {
  161. if (asset == null || !(await asset.exists)) {
  162. if (widget.file.uploadedFileID != null) {
  163. widget.file.localID = null;
  164. FilesDB.instance.update(widget.file);
  165. _loadNetworkImage();
  166. } else {
  167. FilesDB.instance.deleteLocalFile(widget.file.localID);
  168. Bus.instance.fire(LocalPhotosUpdatedEvent([widget.file]));
  169. }
  170. return;
  171. }
  172. asset
  173. .thumbDataWithSize(
  174. THUMBNAIL_SMALL_SIZE,
  175. THUMBNAIL_SMALL_SIZE,
  176. quality: THUMBNAIL_QUALITY,
  177. )
  178. .then((data) {
  179. if (data != null && mounted) {
  180. final imageProvider = Image.memory(data).image;
  181. _cacheAndRender(imageProvider);
  182. }
  183. ThumbnailLruCache.put(widget.file, data, THUMBNAIL_SMALL_SIZE);
  184. });
  185. }).catchError((e) {
  186. _logger.warning("Could not load image: ", e);
  187. _encounteredErrorLoadingThumbnail = true;
  188. });
  189. }
  190. void _loadNetworkImage() {
  191. if (!_hasLoadedThumbnail &&
  192. !_encounteredErrorLoadingThumbnail &&
  193. !_isLoadingThumbnail) {
  194. _isLoadingThumbnail = true;
  195. final cachedThumbnail = ThumbnailLruCache.get(widget.file);
  196. if (cachedThumbnail != null) {
  197. _imageProvider = Image.memory(cachedThumbnail).image;
  198. _hasLoadedThumbnail = true;
  199. return;
  200. }
  201. if (widget.serverLoadDeferDuration != null) {
  202. Future.delayed(widget.serverLoadDeferDuration, () {
  203. if (mounted) {
  204. _getThumbnailFromServer();
  205. }
  206. });
  207. } else {
  208. _getThumbnailFromServer();
  209. }
  210. }
  211. }
  212. void _getThumbnailFromServer() async {
  213. try {
  214. final thumbnail = await getThumbnailFromServer(widget.file);
  215. if (mounted) {
  216. final imageProvider = Image.memory(thumbnail).image;
  217. _cacheAndRender(imageProvider);
  218. }
  219. } catch (e) {
  220. if (e is RequestCancelledError) {
  221. if (mounted) {
  222. _logger.info(
  223. "Thumbnail request was aborted although it is in view, will retry");
  224. _reset();
  225. setState(() {});
  226. }
  227. } else {
  228. _logger.severe("Could not load image " + widget.file.toString(), e);
  229. _encounteredErrorLoadingThumbnail = true;
  230. }
  231. }
  232. }
  233. void _cacheAndRender(ImageProvider<Object> imageProvider) {
  234. if (imageCache.currentSizeBytes > 256 * 1024 * 1024) {
  235. _logger.info("Clearing image cache");
  236. imageCache.clear();
  237. imageCache.clearLiveImages();
  238. }
  239. precacheImage(imageProvider, context).then((value) {
  240. if (mounted) {
  241. setState(() {
  242. _imageProvider = imageProvider;
  243. _hasLoadedThumbnail = true;
  244. });
  245. }
  246. });
  247. }
  248. void _reset() {
  249. _hasLoadedThumbnail = false;
  250. _isLoadingThumbnail = false;
  251. _encounteredErrorLoadingThumbnail = false;
  252. _imageProvider = null;
  253. }
  254. }