diff --git a/lib/db/db_helper.dart b/lib/db/db_helper.dart index 7e4fda016..d0eae2136 100644 --- a/lib/db/db_helper.dart +++ b/lib/db/db_helper.dart @@ -11,11 +11,13 @@ class DatabaseHelper { static final table = 'photos'; + static final columnUploadedFileId = 'uploaded_file_id'; static final columnLocalId = 'local_id'; static final columnLocalPath = 'local_path'; static final columnThumbnailPath = 'thumbnail_path'; static final columnPath = 'path'; static final columnHash = 'hash'; + static final columnIsDeleted = 'is_deleted'; static final columnSyncTimestamp = 'sync_timestamp'; // make this a singleton class @@ -44,10 +46,12 @@ class DatabaseHelper { await db.execute(''' CREATE TABLE $table ( $columnLocalId TEXT, + $columnUploadedFileId INTEGER NOT NULL, $columnLocalPath TEXT NOT NULL, $columnThumbnailPath TEXT NOT NULL, $columnPath TEXT, $columnHash TEXT NOT NULL, + $columnIsDeleted INTEGER DEFAULT 0, $columnSyncTimestamp TEXT ) '''); @@ -75,26 +79,27 @@ class DatabaseHelper { Future> getAllPhotos() async { Database db = await instance.database; - var results = await db.query(table); + var results = await db.query(table, where: '$columnIsDeleted = 0'); + return _convertToPhotos(results); + } + + Future> getAllDeletedPhotos() async { + Database db = await instance.database; + var results = await db.query(table, where: '$columnIsDeleted = 1'); return _convertToPhotos(results); } Future> getPhotosToBeUploaded() async { Database db = await instance.database; - var results = await db.query(table, where: '$columnPath IS NULL'); + var results = await db.query(table, where: '$columnUploadedFileId = -1'); return _convertToPhotos(results); } - // We are assuming here that the hash column in the map is set. The other - // column values will be used to update the row. - Future updatePathAndTimestamp( - String hash, String path, String timestamp) async { + Future updatePhoto(Photo photo) async { Database db = await instance.database; - var row = new Map(); - row[columnPath] = path; - row[columnSyncTimestamp] = timestamp; - return await db - .update(table, row, where: '$columnHash = ?', whereArgs: [hash]); + return await db.update(table, _getRowForPhoto(photo), + where: '$columnHash = ? AND $columnPath = ?', + whereArgs: [photo.hash, photo.localPath]); } Future getPhotoByPath(String path) async { @@ -102,7 +107,7 @@ class DatabaseHelper { var rows = await db.query(table, where: '$columnPath =?', whereArgs: [path]); if (rows.length > 0) { - return Photo.fromRow(rows[0]); + return _getPhotofromRow(rows[0]); } else { throw ("No cached photo"); } @@ -115,10 +120,26 @@ class DatabaseHelper { 0; } + Future markPhotoAsDeleted(Photo photo) async { + Database db = await instance.database; + var values = new Map(); + values[columnIsDeleted] = 1; + return db.update(table, values, + where: '$columnHash =? AND $columnLocalPath =?', + whereArgs: [photo.hash, photo.localPath]); + } + + Future deletePhoto(Photo photo) async { + Database db = await instance.database; + return db.delete(table, + where: '$columnHash =? AND $columnLocalPath =?', + whereArgs: [photo.hash, photo.localPath]); + } + List _convertToPhotos(List> results) { var photos = List(); for (var result in results) { - photos.add(Photo.fromRow(result)); + photos.add(_getPhotofromRow(result)); } return photos; } @@ -126,6 +147,8 @@ class DatabaseHelper { Map _getRowForPhoto(Photo photo) { var row = new Map(); row[columnLocalId] = photo.localId; + row[columnUploadedFileId] = + photo.uploadedFileId == null ? -1 : photo.uploadedFileId; row[columnLocalPath] = photo.localPath; row[columnThumbnailPath] = photo.thumbnailPath; row[columnPath] = photo.path; @@ -133,4 +156,18 @@ class DatabaseHelper { row[columnSyncTimestamp] = photo.syncTimestamp; return row; } + + Photo _getPhotofromRow(Map row) { + Photo photo = Photo(); + photo.localId = row[columnLocalId]; + photo.uploadedFileId = row[columnUploadedFileId]; + photo.localPath = row[columnLocalPath]; + photo.thumbnailPath = row[columnThumbnailPath]; + photo.path = row[columnPath]; + photo.hash = row[columnHash]; + photo.syncTimestamp = row[columnSyncTimestamp] == null + ? -1 + : int.parse(row[columnSyncTimestamp]); + return photo; + } } diff --git a/lib/models/photo.dart b/lib/models/photo.dart index 9e27f8b76..917220094 100644 --- a/lib/models/photo.dart +++ b/lib/models/photo.dart @@ -4,6 +4,7 @@ import 'package:crypto/crypto.dart'; import 'package:photo_manager/photo_manager.dart'; class Photo { + int uploadedFileId; String localId; String path; String localPath; @@ -14,24 +15,16 @@ class Photo { Photo(); Photo.fromJson(Map json) - : path = json["path"], + : uploadedFileId = json["fileId"], + path = json["path"], hash = json["hash"], thumbnailPath = json["thumbnailPath"], syncTimestamp = json["syncTimestamp"]; - Photo.fromRow(Map row) - : localId = row["local_id"], - localPath = row["local_path"], - thumbnailPath = row["thumbnail_path"], - path = row["path"], - hash = row["hash"], - syncTimestamp = row["sync_timestamp"] == null - ? -1 - : int.parse(row["sync_timestamp"]); - static Future fromAsset(AssetEntity asset) async { Photo photo = Photo(); var file = (await asset.originFile); + photo.uploadedFileId = -1; photo.localId = asset.id; photo.localPath = file.path; photo.hash = getHash(file); diff --git a/lib/photo_sync_manager.dart b/lib/photo_sync_manager.dart index b7bfa9cd4..9999ed6a5 100644 --- a/lib/photo_sync_manager.dart +++ b/lib/photo_sync_manager.dart @@ -29,7 +29,9 @@ class PhotoSyncManager { Future init() async { _updateDatabase().then((_) { - _syncPhotos(); + _syncPhotos().then((_) { + _deletePhotos(); + }); }); } @@ -80,9 +82,8 @@ class PhotoSyncManager { List photosToBeUploaded = await DatabaseHelper.instance.getPhotosToBeUploaded(); for (Photo photo in photosToBeUploaded) { - var uploadedPhoto = await _uploadFile(photo.localPath, photo.hash); - await DatabaseHelper.instance.updatePathAndTimestamp(photo.hash, - uploadedPhoto.path, uploadedPhoto.syncTimestamp.toString()); + var uploadedPhoto = await _uploadFile(photo); + await DatabaseHelper.instance.updatePhoto(uploadedPhoto); prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.syncTimestamp); } } @@ -93,10 +94,6 @@ class PhotoSyncManager { var path = externalPath + "/photos/"; for (Photo photo in diff) { if (await DatabaseHelper.instance.containsPhotoHash(photo.hash)) { - await DatabaseHelper.instance.updatePathAndTimestamp( - photo.hash, photo.path, photo.syncTimestamp.toString()); - continue; - } else { var localPath = path + basename(photo.path); await _dio .download(Constants.ENDPOINT + "/" + photo.path, localPath) @@ -126,18 +123,40 @@ class PhotoSyncManager { } } - Future _uploadFile(String path, String hash) async { + Future _uploadFile(Photo localPhoto) async { var formData = FormData.fromMap({ - "file": await MultipartFile.fromFile(path, filename: basename(path)), + "file": await MultipartFile.fromFile(localPhoto.localPath, + filename: basename(localPhoto.localPath)), "user": Constants.USER, }); var response = await _dio .post(Constants.ENDPOINT + "/upload", data: formData) .catchError(_onError); - _logger.i(response.toString()); - var photo = Photo.fromJson(response.data); - photo.localPath = path; - return photo; + if (response != null) { + _logger.i(response.toString()); + var photo = Photo.fromJson(response.data); + photo.localPath = localPhoto.localPath; + photo.localId = localPhoto.localId; + return photo; + } else { + return null; + } + } + + Future _deletePhotos() async { + DatabaseHelper.instance.getAllDeletedPhotos().then((deletedPhotos) { + for (Photo deletedPhoto in deletedPhotos) { + _deletePhotoOnServer(deletedPhoto) + .then((value) => DatabaseHelper.instance.deletePhoto(deletedPhoto)); + } + }); + } + + Future _deletePhotoOnServer(Photo photo) async { + return _dio.post(Constants.ENDPOINT + "/delete", queryParameters: { + "user": Constants.USER, + "fileID": photo.uploadedFileId + }).catchError((e) => _onError(e)); } void _onError(error) { diff --git a/lib/ui/gallery.dart b/lib/ui/gallery.dart index 79789c465..99d957778 100644 --- a/lib/ui/gallery.dart +++ b/lib/ui/gallery.dart @@ -1,9 +1,16 @@ +import 'dart:io'; + +import 'package:draggable_scrollbar/draggable_scrollbar.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; +import 'package:myapp/db/db_helper.dart'; import 'package:myapp/models/photo.dart'; import 'package:myapp/photo_loader.dart'; +import 'package:myapp/photo_sync_manager.dart'; import 'package:myapp/ui/image_widget.dart'; import 'package:provider/provider.dart'; +import 'package:share_extend/share_extend.dart'; import 'package:toast/toast.dart'; import 'change_notifier_builder.dart'; @@ -17,43 +24,27 @@ class Gallery extends StatefulWidget { } class _GalleryState extends State { - Logger _logger = Logger(); - - int _crossAxisCount = 4; - PhotoLoader get photoLoader => Provider.of(context); + final ScrollController _scrollController = ScrollController(); @override Widget build(BuildContext context) { - _logger.i("Build with _crossAxisCount: " + _crossAxisCount.toString()); - return GestureDetector( - onScaleUpdate: (ScaleUpdateDetails details) { - _logger.i("Scale update: " + details.horizontalScale.toString()); - setState(() { - if (details.horizontalScale < 1) { - _crossAxisCount = 8; - } else if (details.horizontalScale < 2) { - _crossAxisCount = 5; - } else if (details.horizontalScale < 4) { - _crossAxisCount = 4; - } else if (details.horizontalScale < 8) { - _crossAxisCount = 2; - } else { - _crossAxisCount = 1; - } - }); - }, - child: ChangeNotifierBuilder( - value: photoLoader, - builder: (_, __) { - return GridView.builder( + return ChangeNotifierBuilder( + value: photoLoader, + builder: (_, __) { + return DraggableScrollbar.semicircle( + labelTextBuilder: (double offset) => Text("Hello!"), + labelConstraints: BoxConstraints.tightFor(width: 80.0, height: 30.0), + controller: _scrollController, + child: GridView.builder( itemBuilder: _buildItem, itemCount: photoLoader.getPhotos().length, + controller: _scrollController, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: _crossAxisCount, - )); - }, - ), + crossAxisCount: 4, + )), + ); + }, ); } @@ -64,7 +55,7 @@ class _GalleryState extends State { routeToDetailPage(photo, context); }, onLongPress: () { - Toast.show(photo.localPath, context); + _showPopup(photo, context); }, child: Padding( padding: const EdgeInsets.all(2.0), @@ -73,6 +64,76 @@ class _GalleryState extends State { ); } + void _showPopup(Photo photo, BuildContext context) { + final action = CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + child: Text("Share"), + isDefaultAction: true, + onPressed: () { + ShareExtend.share(photo.localPath, "image"); + Navigator.pop(context); + }, + ), + CupertinoActionSheetAction( + child: Text("Delete"), + isDestructiveAction: true, + onPressed: () { + Navigator.pop(context); + _showDeletePopup(photo, context); + }, + ) + ], + cancelButton: CupertinoActionSheetAction( + child: Text("Cancel"), + onPressed: () { + Navigator.pop(context); + }, + ), + ); + showCupertinoModalPopup(context: context, builder: (_) => action); + } + + void _showDeletePopup(Photo photo, BuildContext context) { + final action = CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + child: Text("Delete on device"), + isDestructiveAction: true, + onPressed: () { + DatabaseHelper.instance.deletePhoto(photo).then((_) { + File file = File(photo.localPath); + file.delete().then((_) { + photoLoader.reloadPhotos(); + Navigator.pop(context); + }); + }); + }, + ), + CupertinoActionSheetAction( + child: Text("Delete everywhere [WiP]"), + isDestructiveAction: true, + onPressed: () { + DatabaseHelper.instance.markPhotoAsDeleted(photo).then((_) { + File file = File(photo.localPath); + file.delete().then((_) { + photoLoader.reloadPhotos(); + Navigator.pop(context); + }); + }); + }, + ) + ], + cancelButton: CupertinoActionSheetAction( + child: Text("Cancel"), + onPressed: () { + Navigator.pop(context); + }, + ), + ); + showCupertinoModalPopup(context: context, builder: (_) => action); + } + void routeToDetailPage(Photo photo, BuildContext context) { final page = DetailPage(photo); Navigator.of(context).push( diff --git a/lib/ui/image_widget.dart b/lib/ui/image_widget.dart index d8f44a2fd..127d0ca94 100644 --- a/lib/ui/image_widget.dart +++ b/lib/ui/image_widget.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:myapp/core/lru_map.dart'; import 'package:myapp/models/photo.dart'; import 'package:myapp/photo_loader.dart'; -import 'package:myapp/ui/loading_widget.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:provider/provider.dart'; @@ -34,7 +33,7 @@ class _ImageWidgetState extends State { if (cachedImage != null) { image = cachedImage; } else { - if (widget.photo.localId.isNotEmpty) { + if (widget.photo.localId != null) { image = FutureBuilder( future: AssetEntity(id: widget.photo.localId) .thumbDataWithSize(size, size), @@ -47,7 +46,10 @@ class _ImageWidgetState extends State { ImageLruCache.setData(path, size, image); return image; } else { - return loadWidget; + return Container( + alignment: Alignment.center, + color: Colors.grey[500], + ); } }, ); diff --git a/pubspec.lock b/pubspec.lock index f1949430e..0c15a8f8a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -71,6 +71,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.9" + draggable_scrollbar: + dependency: "direct main" + description: + name: draggable_scrollbar + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index e43bb5bb8..ec9b0f682 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: toast: ^0.1.5 image: ^2.1.4 share_extend: "^1.1.2" + draggable_scrollbar: ^0.0.4 dev_dependencies: flutter_test: