thumbnail_widget.dart 7.8 KB

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