Display uploading status on the refresh indicator

This commit is contained in:
Vishnu Mohandas 2020-06-16 00:12:25 +05:30
parent 4d77ccc8ba
commit bdb317956a
4 changed files with 123 additions and 39 deletions

View file

@ -0,0 +1,7 @@
class PhotoUploadEvent {
final int completed;
final int total;
final bool hasError;
PhotoUploadEvent({this.completed, this.total, this.hasError = false});
}

View file

@ -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<void> _existingSync;
static final _lastSyncTimestampKey = "last_sync_timestamp_0";
static final _lastDBUpdateTimestampKey = "last_db_update_timestamp";
@ -40,11 +42,23 @@ class PhotoSyncManager {
Future<void> sync() async {
if (_isSyncInProgress) {
_logger.warning("Sync already in progress, skipping.");
return;
return _existingSync;
}
_isSyncInProgress = true;
_logger.info("Syncing...");
_existingSync = Future<void>(() async {
_logger.info("Syncing...");
try {
await _doSync();
} catch (e) {
throw e;
} finally {
_isSyncInProgress = false;
}
});
return _existingSync;
}
Future<void> _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<void> _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<bool> _updateDatabase(
@ -149,19 +156,22 @@ class PhotoSyncManager {
return lastSyncTimestamp;
}
Future _uploadDiff(SharedPreferences prefs) async {
Future<void> _uploadDiff(SharedPreferences prefs) async {
List<Photo> 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<void> _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);
}
});
}

View file

@ -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<Gallery> {
Set<Photo> _selectedPhotos = HashSet<Photo>();
List<Photo> _photos;
RefreshController _refreshController = RefreshController();
StreamSubscription<PhotoOpenedEvent> _subscription;
StreamSubscription<PhotoOpenedEvent> _photoOpenEventSubscription;
StreamSubscription<PhotoUploadEvent> _photoUploadEventSubscription;
Photo _openedPhoto;
@override
void initState() {
_requiresLoad = true;
_subscription = Bus.instance.on<PhotoOpenedEvent>().listen((event) {
_photoOpenEventSubscription =
Bus.instance.on<PhotoOpenedEvent>().listen((event) {
setState(() {
_openedPhoto = event.photo;
});
});
_photoUploadEventSubscription =
Bus.instance.on<PhotoUploadEvent>().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<Gallery> {
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<Gallery> {
}));
}).catchError((e) {
_refreshController.refreshFailed();
setState(() {});
});
},
);

View file

@ -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<SyncIndicator> {
PhotoUploadEvent _event;
StreamSubscription<PhotoUploadEvent> _subscription;
@override
void initState() {
_subscription = Bus.instance.on<PhotoUploadEvent>().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;
}
}
}