diff --git a/lib/events/photo_upload_event.dart b/lib/events/photo_upload_event.dart new file mode 100644 index 000000000..8be07514f --- /dev/null +++ b/lib/events/photo_upload_event.dart @@ -0,0 +1,7 @@ +class PhotoUploadEvent { + final int completed; + final int total; + final bool hasError; + + PhotoUploadEvent({this.completed, this.total, this.hasError = false}); +} diff --git a/lib/photo_sync_manager.dart b/lib/photo_sync_manager.dart index 2354c2707..99799a681 100644 --- a/lib/photo_sync_manager.dart +++ b/lib/photo_sync_manager.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:logging/logging.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/db/photo_db.dart'; +import 'package:photos/events/photo_upload_event.dart'; import 'package:photos/events/user_authenticated_event.dart'; import 'package:photos/photo_repository.dart'; import 'package:photos/photo_provider.dart'; @@ -23,6 +24,7 @@ class PhotoSyncManager { final _dio = Dio(); final _db = PhotoDB.instance; bool _isSyncInProgress = false; + Future _existingSync; static final _lastSyncTimestampKey = "last_sync_timestamp_0"; static final _lastDBUpdateTimestampKey = "last_db_update_timestamp"; @@ -40,11 +42,23 @@ class PhotoSyncManager { Future sync() async { if (_isSyncInProgress) { _logger.warning("Sync already in progress, skipping."); - return; + return _existingSync; } _isSyncInProgress = true; - _logger.info("Syncing..."); + _existingSync = Future(() async { + _logger.info("Syncing..."); + try { + await _doSync(); + } catch (e) { + throw e; + } finally { + _isSyncInProgress = false; + } + }); + return _existingSync; + } + Future _doSync() async { final prefs = await SharedPreferences.getInstance(); final syncStartTimestamp = DateTime.now().microsecondsSinceEpoch; var lastDBUpdateTimestamp = prefs.getInt(_lastDBUpdateTimestampKey); @@ -69,18 +83,13 @@ class PhotoSyncManager { await _addToPhotos(recents, lastDBUpdateTimestamp, photos); } - if (photos.isEmpty) { - _isSyncInProgress = false; - _syncWithRemote(prefs); - } else { + if (photos.isNotEmpty) { photos.sort((first, second) => first.createTimestamp.compareTo(second.createTimestamp)); - _updateDatabase(photos, prefs, lastDBUpdateTimestamp, syncStartTimestamp) - .then((_) { - _isSyncInProgress = false; - _syncWithRemote(prefs); - }); + await _updateDatabase( + photos, prefs, lastDBUpdateTimestamp, syncStartTimestamp); } + await _syncWithRemote(prefs); } Future _addToPhotos(AssetPathEntity pathEntity, int lastDBUpdateTimestamp, @@ -102,17 +111,15 @@ class PhotoSyncManager { } } - _syncWithRemote(SharedPreferences prefs) { + Future _syncWithRemote(SharedPreferences prefs) async { // TODO: Fix race conditions triggered due to concurrent syncs. // Add device_id/last_sync_timestamp to the upload request? if (!Configuration.instance.hasConfiguredAccount()) { - return; + return Future.error("Account not configured yet"); } - _downloadDiff(prefs).then((_) { - _uploadDiff(prefs).then((_) { - _deletePhotosOnServer(); - }); - }); + await _downloadDiff(prefs); + await _uploadDiff(prefs); + await _deletePhotosOnServer(); } Future _updateDatabase( @@ -149,19 +156,22 @@ class PhotoSyncManager { return lastSyncTimestamp; } - Future _uploadDiff(SharedPreferences prefs) async { + Future _uploadDiff(SharedPreferences prefs) async { List photosToBeUploaded = await _db.getPhotosToBeUploaded(); - for (Photo photo in photosToBeUploaded) { + for (int i = 0; i < photosToBeUploaded.length; i++) { + Photo photo = photosToBeUploaded[i]; + _logger.info("Uploading " + photo.toString()); try { var uploadedPhoto = await _uploadFile(photo); - if (uploadedPhoto == null) { - return; - } await _db.updatePhoto(photo.generatedId, uploadedPhoto.uploadedFileId, uploadedPhoto.remotePath, uploadedPhoto.updateTimestamp); prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.updateTimestamp); + + Bus.instance.fire(PhotoUploadEvent( + completed: i + 1, total: photosToBeUploaded.length)); } catch (e) { - _logger.severe(e); + Bus.instance.fire(PhotoUploadEvent(hasError: true)); + throw e; } } } @@ -222,16 +232,14 @@ class PhotoSyncManager { ) .then((response) { return Photo.fromJson(response.data); - }).catchError((e) { - _logger.severe("Error in uploading ", e); }); } Future _deletePhotosOnServer() async { - _db.getAllDeletedPhotos().then((deletedPhotos) { + return _db.getAllDeletedPhotos().then((deletedPhotos) async { for (Photo deletedPhoto in deletedPhotos) { - _deletePhotoOnServer(deletedPhoto) - .then((value) => _db.deletePhoto(deletedPhoto)); + await _deletePhotoOnServer(deletedPhoto); + await _db.deletePhoto(deletedPhoto); } }); } diff --git a/lib/ui/gallery.dart b/lib/ui/gallery.dart index 608c80cc1..93e49b948 100644 --- a/lib/ui/gallery.dart +++ b/lib/ui/gallery.dart @@ -6,9 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/photo_opened_event.dart'; +import 'package:photos/events/photo_upload_event.dart'; import 'package:photos/models/photo.dart'; import 'package:photos/ui/detail_page.dart'; import 'package:photos/ui/loading_widget.dart'; +import 'package:photos/ui/sync_indicator.dart'; import 'package:photos/ui/thumbnail_widget.dart'; import 'package:photos/utils/date_time_util.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; @@ -41,23 +43,32 @@ class _GalleryState extends State { Set _selectedPhotos = HashSet(); List _photos; RefreshController _refreshController = RefreshController(); - StreamSubscription _subscription; + StreamSubscription _photoOpenEventSubscription; + StreamSubscription _photoUploadEventSubscription; Photo _openedPhoto; @override void initState() { _requiresLoad = true; - _subscription = Bus.instance.on().listen((event) { + _photoOpenEventSubscription = + Bus.instance.on().listen((event) { setState(() { _openedPhoto = event.photo; }); }); + _photoUploadEventSubscription = + Bus.instance.on().listen((event) { + if (event.hasError) { + _refreshController.refreshFailed(); + } + }); super.initState(); } @override void dispose() { - _subscription.cancel(); + _photoOpenEventSubscription.cancel(); + _photoUploadEventSubscription.cancel(); super.dispose(); } @@ -103,13 +114,7 @@ class _GalleryState extends State { return SmartRefresher( controller: _refreshController, child: list, - header: ClassicHeader( - idleText: "Pull down to sync.", - refreshingText: "Syncing...", - releaseText: "Release to sync.", - completeText: "Sync completed.", - failedText: "Sync unsuccessful.", - ), + header: SyncIndicator(), onRefresh: () { widget.syncFunction().then((_) { _refreshController.refreshCompleted(); @@ -118,6 +123,7 @@ class _GalleryState extends State { })); }).catchError((e) { _refreshController.refreshFailed(); + setState(() {}); }); }, ); diff --git a/lib/ui/sync_indicator.dart b/lib/ui/sync_indicator.dart new file mode 100644 index 000000000..b669e1a71 --- /dev/null +++ b/lib/ui/sync_indicator.dart @@ -0,0 +1,63 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:photos/core/event_bus.dart'; +import 'package:photos/events/photo_upload_event.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +class SyncIndicator extends StatefulWidget { + @override + _SyncIndicatorState createState() => _SyncIndicatorState(); +} + +class _SyncIndicatorState extends State { + PhotoUploadEvent _event; + StreamSubscription _subscription; + + @override + void initState() { + _subscription = Bus.instance.on().listen((event) { + setState(() { + _event = event; + }); + }); + super.initState(); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ClassicHeader( + idleText: "Pull down to sync.", + refreshingText: _getRefreshingText(), + releaseText: "Release to sync.", + completeText: "Sync completed.", + failedText: "Sync unsuccessful.", + completeDuration: const Duration(milliseconds: 600), + refreshStyle: RefreshStyle.UnFollow, + ); + } + + String _getRefreshingText() { + if (_event == null) { + return "Syncing..."; + } else { + var s; + if (_event.hasError) { + s = "Upload failed."; + } else { + s = "Uploading " + + _event.completed.toString() + + "/" + + _event.total.toString(); + } + _event = null; + return s; + } + } +}