thumbnail_widget.dart 10 KB

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