thumbnail_widget.dart 9.4 KB

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