123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- import 'package:flutter/material.dart';
- import 'package:logging/logging.dart';
- import 'package:photos/core/cache/thumbnail_cache.dart';
- import 'package:photos/core/constants.dart';
- import 'package:photos/core/errors.dart';
- import 'package:photos/core/event_bus.dart';
- import 'package:photos/db/files_db.dart';
- import 'package:photos/db/trash_db.dart';
- import 'package:photos/events/files_updated_event.dart';
- import 'package:photos/events/local_photos_updated_event.dart';
- import 'package:photos/models/file.dart';
- import 'package:photos/models/file_type.dart';
- import 'package:photos/models/trash_file.dart';
- import 'package:photos/ui/viewer/file/file_icons_widget.dart';
- import 'package:photos/utils/file_util.dart';
- import 'package:photos/utils/thumbnail_util.dart';
- class ThumbnailWidget extends StatefulWidget {
- final File file;
- final BoxFit fit;
- final bool shouldShowSyncStatus;
- final bool shouldShowArchiveStatus;
- final bool shouldShowLivePhotoOverlay;
- final Duration diskLoadDeferDuration;
- final Duration serverLoadDeferDuration;
- ThumbnailWidget(
- this.file, {
- Key key,
- this.fit = BoxFit.cover,
- this.shouldShowSyncStatus = true,
- this.shouldShowLivePhotoOverlay = false,
- this.shouldShowArchiveStatus = false,
- this.diskLoadDeferDuration,
- this.serverLoadDeferDuration,
- }) : super(key: key ?? Key(file.tag()));
- @override
- State<ThumbnailWidget> createState() => _ThumbnailWidgetState();
- }
- class _ThumbnailWidgetState extends State<ThumbnailWidget> {
- static final _logger = Logger("ThumbnailWidget");
- bool _hasLoadedThumbnail = false;
- bool _isLoadingLocalThumbnail = false;
- bool _errorLoadingLocalThumbnail = false;
- bool _isLoadingRemoteThumbnail = false;
- bool _errorLoadingRemoteThumbnail = false;
- ImageProvider _imageProvider;
- @override
- void initState() {
- super.initState();
- }
- @override
- void dispose() {
- super.dispose();
- Future.delayed(const Duration(milliseconds: 10), () {
- // Cancel request only if the widget has been unmounted
- if (!mounted && widget.file.isRemoteFile() && !_hasLoadedThumbnail) {
- removePendingGetThumbnailRequestIfAny(widget.file);
- }
- });
- }
- @override
- void didUpdateWidget(ThumbnailWidget oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (widget.file.generatedID != oldWidget.file.generatedID) {
- _reset();
- }
- }
- @override
- Widget build(BuildContext context) {
- if (widget.file.isRemoteFile()) {
- _loadNetworkImage();
- } else {
- _loadLocalImage(context);
- }
- Widget image;
- if (_imageProvider != null) {
- image = Image(
- image: _imageProvider,
- fit: widget.fit,
- );
- }
- // todo: [2ndJuly22] pref-review if the content Widget which depends on
- // thumbnail fetch logic should be part of separate stateFull widget.
- // If yes, parent thumbnail widget can be stateless
- Widget content;
- if (image != null) {
- final List<Widget> contentChildren = [image];
- if (widget.file.fileType == FileType.video) {
- contentChildren.add(const VideoOverlayIcon());
- } else if (widget.file.fileType == FileType.livePhoto &&
- widget.shouldShowLivePhotoOverlay) {
- contentChildren.add(const LivePhotoOverlayIcon());
- }
- content = contentChildren.length == 1
- ? contentChildren.first
- : Stack(
- fit: StackFit.expand,
- children: contentChildren,
- );
- }
- final List<Widget> viewChildren = [
- const ThumbnailPlaceHolder(),
- AnimatedOpacity(
- opacity: content == null ? 0 : 1.0,
- duration: const Duration(milliseconds: 200),
- child: content,
- )
- ];
- if (widget.shouldShowSyncStatus && widget.file.uploadedFileID == null) {
- viewChildren.add(const UnSyncedIcon());
- }
- if (widget.file is TrashFile) {
- viewChildren.add(TrashedFileOverlayText(widget.file));
- }
- // todo: Move this icon overlay to the collection widget.
- if (widget.shouldShowArchiveStatus) {
- viewChildren.add(const ArchiveOverlayIcon());
- }
- return Stack(
- fit: StackFit.expand,
- children: viewChildren,
- );
- }
- void _loadLocalImage(BuildContext context) {
- if (!_hasLoadedThumbnail &&
- !_errorLoadingLocalThumbnail &&
- !_isLoadingLocalThumbnail) {
- _isLoadingLocalThumbnail = true;
- final cachedSmallThumbnail =
- ThumbnailLruCache.get(widget.file, kThumbnailSmallSize);
- if (cachedSmallThumbnail != null) {
- _imageProvider = Image.memory(cachedSmallThumbnail).image;
- _hasLoadedThumbnail = true;
- } else {
- if (widget.diskLoadDeferDuration != null) {
- Future.delayed(widget.diskLoadDeferDuration, () {
- if (mounted) {
- _getThumbnailFromDisk();
- }
- });
- } else {
- _getThumbnailFromDisk();
- }
- }
- }
- }
- Future _getThumbnailFromDisk() async {
- getThumbnailFromLocal(widget.file).then((thumbData) async {
- if (thumbData == null) {
- if (widget.file.uploadedFileID != null) {
- _logger.fine("Removing localID reference for " + widget.file.tag());
- widget.file.localID = null;
- if (widget.file is TrashFile) {
- TrashDB.instance.update(widget.file);
- } else {
- FilesDB.instance.update(widget.file);
- }
- _loadNetworkImage();
- } else {
- if (await doesLocalFileExist(widget.file) == false) {
- _logger.info("Deleting file " + widget.file.tag());
- FilesDB.instance.deleteLocalFile(widget.file);
- Bus.instance.fire(
- LocalPhotosUpdatedEvent(
- [widget.file],
- type: EventType.deletedFromDevice,
- ),
- );
- }
- }
- return;
- }
- if (thumbData != null && mounted) {
- final imageProvider = Image.memory(thumbData).image;
- _cacheAndRender(imageProvider);
- }
- ThumbnailLruCache.put(widget.file, thumbData, kThumbnailSmallSize);
- }).catchError((e) {
- _logger.warning("Could not load image: ", e);
- _errorLoadingLocalThumbnail = true;
- });
- }
- void _loadNetworkImage() {
- if (!_hasLoadedThumbnail &&
- !_errorLoadingRemoteThumbnail &&
- !_isLoadingRemoteThumbnail) {
- _isLoadingRemoteThumbnail = true;
- final cachedThumbnail = ThumbnailLruCache.get(widget.file);
- if (cachedThumbnail != null) {
- _imageProvider = Image.memory(cachedThumbnail).image;
- _hasLoadedThumbnail = true;
- return;
- }
- if (widget.serverLoadDeferDuration != null) {
- Future.delayed(widget.serverLoadDeferDuration, () {
- if (mounted) {
- _getThumbnailFromServer();
- }
- });
- } else {
- _getThumbnailFromServer();
- }
- }
- }
- void _getThumbnailFromServer() async {
- try {
- final thumbnail = await getThumbnailFromServer(widget.file);
- if (mounted) {
- final imageProvider = Image.memory(thumbnail).image;
- _cacheAndRender(imageProvider);
- }
- } catch (e) {
- if (e is RequestCancelledError) {
- if (mounted) {
- _logger.info(
- "Thumbnail request was aborted although it is in view, will retry",
- );
- _reset();
- setState(() {});
- }
- } else {
- _logger.severe("Could not load image " + widget.file.toString(), e);
- _errorLoadingRemoteThumbnail = true;
- }
- }
- }
- void _cacheAndRender(ImageProvider<Object> imageProvider) {
- if (imageCache.currentSizeBytes > 256 * 1024 * 1024) {
- _logger.info("Clearing image cache");
- imageCache.clear();
- imageCache.clearLiveImages();
- }
- precacheImage(imageProvider, context).then((value) {
- if (mounted) {
- setState(() {
- _imageProvider = imageProvider;
- _hasLoadedThumbnail = true;
- });
- }
- });
- }
- void _reset() {
- _hasLoadedThumbnail = false;
- _isLoadingLocalThumbnail = false;
- _isLoadingRemoteThumbnail = false;
- _errorLoadingLocalThumbnail = false;
- _errorLoadingRemoteThumbnail = false;
- _imageProvider = null;
- }
- }
|