diff --git a/lib/db/db_helper.dart b/lib/db/db_helper.dart index 9ab79e3ca..cb682d131 100644 --- a/lib/db/db_helper.dart +++ b/lib/db/db_helper.dart @@ -14,11 +14,9 @@ class DatabaseHelper { static final columnGeneratedId = '_id'; static final columnUploadedFileId = 'uploaded_file_id'; static final columnLocalId = 'local_id'; - static final columnLocalPath = 'local_path'; - static final columnRelativePath = 'relative_path'; - static final columnThumbnailPath = 'thumbnail_path'; - static final columnPath = 'path'; - static final columnHash = 'hash'; + static final columnTitle = 'title'; + static final columnPathName = 'path_name'; + static final columnRemotePath = 'remote_path'; static final columnIsDeleted = 'is_deleted'; static final columnCreateTimestamp = 'create_timestamp'; static final columnSyncTimestamp = 'sync_timestamp'; @@ -51,11 +49,9 @@ class DatabaseHelper { $columnGeneratedId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, $columnLocalId TEXT, $columnUploadedFileId INTEGER NOT NULL, - $columnLocalPath TEXT NOT NULL, - $columnRelativePath TEXT NOT NULL, - $columnThumbnailPath TEXT NOT NULL, - $columnPath TEXT, - $columnHash TEXT NOT NULL, + $columnTitle TEXT NOT NULL, + $columnPathName TEXT NOT NULL, + $columnRemotePath TEXT, $columnIsDeleted INTEGER DEFAULT 0, $columnCreateTimestamp TEXT NOT NULL, $columnSyncTimestamp TEXT @@ -103,16 +99,20 @@ class DatabaseHelper { return _convertToPhotos(results); } - Future updatePhoto(Photo photo) async { + Future updatePhoto( + int generatedId, String remotePath, int syncTimestamp) async { Database db = await instance.database; - return await db.update(table, _getRowForPhoto(photo), - where: '$columnGeneratedId = ?', whereArgs: [photo.generatedId]); + var values = new Map(); + values[columnRemotePath] = remotePath; + values[columnSyncTimestamp] = syncTimestamp; + return await db.update(table, values, + where: '$columnGeneratedId = ?', whereArgs: [generatedId]); } Future getPhotoByPath(String path) async { Database db = await instance.database; var rows = - await db.query(table, where: '$columnPath =?', whereArgs: [path]); + await db.query(table, where: '$columnRemotePath =?', whereArgs: [path]); if (rows.length > 0) { return _getPhotofromRow(rows[0]); } else { @@ -147,11 +147,9 @@ class DatabaseHelper { row[columnLocalId] = photo.localId; row[columnUploadedFileId] = photo.uploadedFileId == null ? -1 : photo.uploadedFileId; - row[columnLocalPath] = photo.localPath; - row[columnRelativePath] = photo.relativePath; - row[columnThumbnailPath] = photo.thumbnailPath; - row[columnPath] = photo.path; - row[columnHash] = photo.hash; + row[columnTitle] = photo.title; + row[columnPathName] = photo.pathName; + row[columnRemotePath] = photo.remotePath; row[columnCreateTimestamp] = photo.createTimestamp; row[columnSyncTimestamp] = photo.syncTimestamp; return row; @@ -162,11 +160,9 @@ class DatabaseHelper { photo.generatedId = row[columnGeneratedId]; photo.localId = row[columnLocalId]; photo.uploadedFileId = row[columnUploadedFileId]; - photo.localPath = row[columnLocalPath]; - photo.relativePath = row[columnRelativePath]; - photo.thumbnailPath = row[columnThumbnailPath]; - photo.path = row[columnPath]; - photo.hash = row[columnHash]; + photo.title = row[columnTitle]; + photo.pathName = row[columnPathName]; + photo.remotePath = row[columnRemotePath]; photo.createTimestamp = int.parse(row[columnCreateTimestamp]); photo.syncTimestamp = row[columnSyncTimestamp] == null ? -1 diff --git a/lib/main.dart b/lib/main.dart index 404eb306d..73c199df1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,9 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; import 'package:myapp/photo_loader.dart'; import 'package:myapp/photo_provider.dart'; import 'package:myapp/photo_sync_manager.dart'; import 'package:myapp/ui/home_widget.dart'; -import 'package:photo_manager/photo_manager.dart'; import 'package:provider/provider.dart'; final provider = PhotoProvider(); @@ -15,20 +12,7 @@ final logger = Logger(); void main() async { WidgetsFlutterBinding.ensureInitialized(); runApp(MyApp()); - await provider.refreshGalleryList(); - - if (provider.list.length > 0) { - provider.list[0].assetList.then((assets) { - init(assets); - }); - } else { - init(List()); - } -} - -Future init(List assets) async { - var photoSyncManager = PhotoSyncManager(assets); - photoSyncManager.init(); + provider.refreshGalleryList().then((_) => PhotoSyncManager(provider.list)); } class MyApp extends StatelessWidget { diff --git a/lib/models/photo.dart b/lib/models/photo.dart index d5dd3763f..6fb10dd26 100644 --- a/lib/models/photo.dart +++ b/lib/models/photo.dart @@ -1,63 +1,81 @@ -import 'dart:convert'; -import 'dart:io'; +import 'dart:typed_data'; -import 'package:crypto/crypto.dart'; -import 'package:logger/logger.dart'; -import 'package:path/path.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:photo_manager/photo_manager.dart'; +import 'package:path/path.dart'; class Photo { int generatedId; int uploadedFileId; String localId; - String path; - String localPath; - String relativePath; - String thumbnailPath; - String hash; + String title; + String pathName; + String remotePath; int createTimestamp; int syncTimestamp; Photo(); - Photo.fromJson(Map json) : uploadedFileId = json["fileId"], - path = json["path"], - hash = json["hash"], - thumbnailPath = json["thumbnailPath"], + remotePath = json["path"], createTimestamp = json["createTimestamp"], syncTimestamp = json["syncTimestamp"]; - static Future fromAsset(AssetEntity asset) async { + static Future fromAsset( + AssetPathEntity pathEntity, AssetEntity asset) async { Photo photo = Photo(); photo.uploadedFileId = -1; photo.localId = asset.id; - var file = await asset.originFile; - photo.localPath = file.path; - if (Platform.isAndroid) { - photo.relativePath = dirname((asset.relativePath.endsWith("/") - ? asset.relativePath - : asset.relativePath + "/") + - asset.title); - } else { - photo.relativePath = dirname(photo.localPath); - } - photo.hash = getHash(file); - photo.thumbnailPath = photo.localPath; + photo.title = asset.title; + photo.pathName = pathEntity.name; photo.createTimestamp = asset.createDateTime.microsecondsSinceEpoch; return photo; } - AssetEntity getAsset() { - return AssetEntity(id: localId); + Future getBytes() { + final asset = AssetEntity(id: localId); + if (extension(title) == ".HEIC") { + return asset.originBytes.then((bytes) => + FlutterImageCompress.compressWithList(bytes) + .then((result) => Uint8List.fromList(result))); + } else { + return asset.originBytes; + } } - static String getHash(File file) { - return sha256.convert(file.readAsBytesSync()).toString(); + Future getOriginalBytes() { + return AssetEntity(id: localId).originBytes; } @override String toString() { - return 'Photo(generatedId: $generatedId, uploadedFileId: $uploadedFileId, localId: $localId, path: $path, localPath: $localPath, relativePath: $relativePath, thumbnailPath: $thumbnailPath, hash: $hash, createTimestamp: $createTimestamp, syncTimestamp: $syncTimestamp)'; + return 'Photo(generatedId: $generatedId, uploadedFileId: $uploadedFileId, localId: $localId, title: $title, pathName: $pathName, remotePath: $remotePath, createTimestamp: $createTimestamp, syncTimestamp: $syncTimestamp)'; + } + + @override + bool operator ==(Object o) { + if (identical(this, o)) return true; + + return o is Photo && + o.generatedId == generatedId && + o.uploadedFileId == uploadedFileId && + o.localId == localId && + o.title == title && + o.pathName == pathName && + o.remotePath == remotePath && + o.createTimestamp == createTimestamp && + o.syncTimestamp == syncTimestamp; + } + + @override + int get hashCode { + return generatedId.hashCode ^ + uploadedFileId.hashCode ^ + localId.hashCode ^ + title.hashCode ^ + pathName.hashCode ^ + remotePath.hashCode ^ + createTimestamp.hashCode ^ + syncTimestamp.hashCode; } } diff --git a/lib/photo_provider.dart b/lib/photo_provider.dart index b8a62aa19..005c38430 100644 --- a/lib/photo_provider.dart +++ b/lib/photo_provider.dart @@ -88,8 +88,10 @@ class PhotoProvider extends ChangeNotifier { if (!result) { print("Did not get permission"); } - var galleryList = - await PhotoManager.getAssetPathList(type: RequestType.image); + final filterOptionGroup = FilterOptionGroup(); + filterOptionGroup.setOption(AssetType.image, FilterOption(needTitle: true)); + var galleryList = await PhotoManager.getAssetPathList( + type: RequestType.image, filterOption: filterOptionGroup); galleryList.sort((s1, s2) { return s2.assetCount.compareTo(s1.assetCount); diff --git a/lib/photo_sync_manager.dart b/lib/photo_sync_manager.dart index 82fef4af3..31433c27b 100644 --- a/lib/photo_sync_manager.dart +++ b/lib/photo_sync_manager.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'dart:math'; import 'package:logger/logger.dart'; import 'package:myapp/db/db_helper.dart'; @@ -16,51 +15,58 @@ import 'package:myapp/core/constants.dart' as Constants; class PhotoSyncManager { final _logger = Logger(); final _dio = Dio(); - final List _assets; static final _lastSyncTimestampKey = "last_sync_timestamp_0"; static final _lastDBUpdateTimestampKey = "last_db_update_timestamp"; - PhotoSyncManager(this._assets) { - _logger.i("PhotoSyncManager init"); - _assets.sort((first, second) => first.createDateTime.microsecondsSinceEpoch - .compareTo(second.createDateTime.microsecondsSinceEpoch)); + PhotoSyncManager(List pathEntities) { + init(pathEntities); } - Future init() async { - _updateDatabase().then((_) { - _syncPhotos().then((_) { - _deletePhotos(); - }); - }); - } - - Future _updateDatabase() async { + Future init(List pathEntities) async { final prefs = await SharedPreferences.getInstance(); var lastDBUpdateTimestamp = prefs.getInt(_lastDBUpdateTimestampKey); if (lastDBUpdateTimestamp == null) { lastDBUpdateTimestamp = 0; await _initializeDirectories(); } - var photos = List(); - var bufferLimit = 10; - final maxBufferLimit = 1000; - for (AssetEntity asset in _assets) { - if (asset.createDateTime.microsecondsSinceEpoch > lastDBUpdateTimestamp) { - try { - photos.add(await Photo.fromAsset(asset)); - } catch (e) { - _logger.e(e); - } - if (photos.length > bufferLimit) { - await _insertPhotosToDB( - photos, prefs, asset.createDateTime.microsecondsSinceEpoch); - photos.clear(); - bufferLimit = min(maxBufferLimit, bufferLimit * 2); + + final photos = List(); + for (AssetPathEntity pathEntity in pathEntities) { + if (Platform.isIOS || pathEntity.name != "Recent") { + // "Recents" contain duplicate information on Android + var assetList = await pathEntity.assetList; + for (AssetEntity entity in assetList) { + if (entity.createDateTime.microsecondsSinceEpoch > + lastDBUpdateTimestamp) { + try { + photos.add(await Photo.fromAsset(pathEntity, entity)); + } catch (e) { + _logger.e(e); + } + } } } } + photos.sort((first, second) => + first.createTimestamp.compareTo(second.createTimestamp)); + + _updateDatabase(photos, prefs, lastDBUpdateTimestamp).then((_) { + _syncPhotos().then((_) { + _deletePhotos(); + }); + }); + } + + Future _updateDatabase(final List photos, + SharedPreferences prefs, int lastDBUpdateTimestamp) async { + var photosToBeAdded = List(); + for (Photo photo in photos) { + if (photo.createTimestamp > lastDBUpdateTimestamp) { + photosToBeAdded.add(photo); + } + } return await _insertPhotosToDB( - photos, prefs, DateTime.now().microsecondsSinceEpoch); + photosToBeAdded, prefs, DateTime.now().microsecondsSinceEpoch); } _syncPhotos() async { @@ -89,7 +95,8 @@ class PhotoSyncManager { if (uploadedPhoto == null) { return; } - await DatabaseHelper.instance.updatePhoto(uploadedPhoto); + await DatabaseHelper.instance.updatePhoto(photo.generatedId, + uploadedPhoto.remotePath, uploadedPhoto.syncTimestamp); prefs.setInt(_lastSyncTimestampKey, uploadedPhoto.syncTimestamp); } } @@ -98,12 +105,12 @@ class PhotoSyncManager { var externalPath = (await getApplicationDocumentsDirectory()).path; var path = externalPath + "/photos/"; for (Photo photo in diff) { - var localPath = path + basename(photo.path); + var localPath = path + basename(photo.remotePath); await _dio - .download(Constants.ENDPOINT + "/" + photo.path, localPath) + .download(Constants.ENDPOINT + "/" + photo.remotePath, localPath) .catchError(_onError); - photo.localPath = localPath; - photo.thumbnailPath = localPath; + // TODO: Save path + photo.pathName = localPath; await DatabaseHelper.instance.insertPhoto(photo); PhotoLoader.instance.reloadPhotos(); await prefs.setInt(_lastSyncTimestampKey, photo.syncTimestamp); @@ -128,8 +135,8 @@ class PhotoSyncManager { Future _uploadFile(Photo localPhoto) async { var formData = FormData.fromMap({ - "file": await MultipartFile.fromFile(localPhoto.localPath, - filename: basename(localPhoto.localPath)), + "file": MultipartFile.fromBytes(await localPhoto.getOriginalBytes()), + "filename": localPhoto.title, "user": Constants.USER, }); return _dio @@ -137,8 +144,6 @@ class PhotoSyncManager { .then((response) { _logger.i(response.toString()); var photo = Photo.fromJson(response.data); - photo.localPath = localPhoto.localPath; - photo.localId = localPhoto.localId; return photo; }).catchError(_onError); } diff --git a/lib/ui/album_list_widget.dart b/lib/ui/album_list_widget.dart index a4198a08e..f19c09830 100644 --- a/lib/ui/album_list_widget.dart +++ b/lib/ui/album_list_widget.dart @@ -42,7 +42,7 @@ class _AlbumListWidgetState extends State { List _getAlbums(List photos) { final albumMap = new LinkedHashMap>(); for (Photo photo in photos) { - final folder = path.basename(photo.relativePath); + final folder = path.basename(photo.pathName); if (!albumMap.containsKey(folder)) { albumMap[folder] = new List(); } @@ -59,7 +59,7 @@ class _AlbumListWidgetState extends State { return GestureDetector( child: Column( children: [ - ImageWidget(album.photos[0], size: 160), + ImageWidget(album.photos[0], size: 140), Padding(padding: EdgeInsets.all(2)), Expanded( child: Text( diff --git a/lib/ui/detail_page.dart b/lib/ui/detail_page.dart index 02e932f89..997526404 100644 --- a/lib/ui/detail_page.dart +++ b/lib/ui/detail_page.dart @@ -4,13 +4,10 @@ import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; import 'package:myapp/core/lru_map.dart'; import 'package:myapp/models/photo.dart'; -import 'package:photo_manager/photo_manager.dart'; import 'package:photo_view/photo_view.dart'; -import 'package:share_extend/share_extend.dart'; import 'extents_page_view.dart'; import 'loading_widget.dart'; -import 'package:path/path.dart' as path; -import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:myapp/utils/share_util.dart'; class DetailPage extends StatefulWidget { final List photos; @@ -27,11 +24,6 @@ class _DetailPageState extends State { int _selectedIndex = 0; final _cachedImages = LRUMap(5); - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { _selectedIndex = widget.selectedIndex; @@ -77,8 +69,8 @@ class _DetailPageState extends State { actions: [ IconButton( icon: Icon(Icons.share), - onPressed: () { - ShareExtend.share(widget.photos[_selectedIndex].localPath, "image"); + onPressed: () async { + share(widget.photos[_selectedIndex]); }, ) ], @@ -99,26 +91,17 @@ class ZoomableImage extends StatelessWidget { @override Widget build(BuildContext context) { - Logger().i("Building " + photo.generatedId.toString()); + Logger().i("Building " + photo.toString()); if (ImageLruCache.getData(photo.generatedId) != null) { return _buildPhotoView(ImageLruCache.getData(photo.generatedId)); } - var future; - if (path.extension(photo.localPath) == '.HEIC') { - Logger().i("Decoding HEIC"); - future = photo.getAsset().originBytes.then((bytes) => - FlutterImageCompress.compressWithList(bytes) - .then((result) => Uint8List.fromList(result))); - } else { - future = AssetEntity(id: photo.localId).originBytes; - } return FutureBuilder( - future: future, + future: photo.getBytes(), builder: (_, snapshot) { if (snapshot.hasData) { return _buildPhotoView(snapshot.data); } else if (snapshot.hasError) { - return Text(snapshot.error); + return Text(snapshot.error.toString()); } else { return loadWidget; } diff --git a/lib/ui/gallery_app_bar_widget.dart b/lib/ui/gallery_app_bar_widget.dart index 9a83c3a40..bcca6aa23 100644 --- a/lib/ui/gallery_app_bar_widget.dart +++ b/lib/ui/gallery_app_bar_widget.dart @@ -1,12 +1,11 @@ -import 'dart:io'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:myapp/db/db_helper.dart'; import 'package:myapp/models/photo.dart'; import 'package:myapp/photo_loader.dart'; +import 'package:photo_manager/photo_manager.dart'; import 'package:provider/provider.dart'; -import 'package:share_extend/share_extend.dart'; +import 'package:myapp/utils/share_util.dart'; class GalleryAppBarWidget extends StatefulWidget implements PreferredSizeWidget { @@ -65,11 +64,7 @@ class _GalleryAppBarWidgetState extends State { } void _shareSelectedPhotos(BuildContext context) { - var photoPaths = List(); - for (Photo photo in widget.selectedPhotos) { - photoPaths.add(photo.localPath); - } - ShareExtend.shareMultiple(photoPaths, "image"); + shareMultiple(widget.selectedPhotos.toList()); } void _showDeletePhotosSheet(BuildContext context) { @@ -102,14 +97,14 @@ class _GalleryAppBarWidgetState extends State { Future _deleteSelectedPhotos( BuildContext context, bool deleteEverywhere) async { + await PhotoManager.editor + .deleteWithIds(widget.selectedPhotos.map((p) => p.localId).toList()); + for (Photo photo in widget.selectedPhotos) { deleteEverywhere ? await DatabaseHelper.instance.markPhotoForDeletion(photo) : await DatabaseHelper.instance.deletePhoto(photo); - File file = File(photo.localPath); - await file.delete(); } - Navigator.of(context, rootNavigator: true).pop(); photoLoader.reloadPhotos(); if (widget.onPhotosDeleted != null) { diff --git a/lib/ui/image_widget.dart b/lib/ui/image_widget.dart index 6c1446d2c..7432ca2bf 100644 --- a/lib/ui/image_widget.dart +++ b/lib/ui/image_widget.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -33,7 +32,7 @@ class _ImageWidgetState extends State { Widget image; if (cachedImageData != null) { - image = Image.memory(cachedImageData); + image = _buildImage(cachedImageData, size); } else { if (widget.photo.localId != null) { image = FutureBuilder( @@ -43,10 +42,7 @@ class _ImageWidgetState extends State { if (snapshot.hasData) { ImageLruCache.setData( widget.photo.generatedId, size, snapshot.data); - Image image = Image.memory(snapshot.data, - width: size.toDouble(), - height: size.toDouble(), - fit: BoxFit.cover); + Image image = _buildImage(snapshot.data, size); return image; } else { return loadingWidget; @@ -54,19 +50,16 @@ class _ImageWidgetState extends State { }, ); } else { - image = Image.file(File(widget.photo.localPath), - width: size.toDouble(), height: size.toDouble(), fit: BoxFit.cover); + // TODO + return Text("Not Implemented"); } } return image; } - @override - void didUpdateWidget(ImageWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.photo.generatedId != oldWidget.photo.generatedId) { - setState(() {}); - } + Image _buildImage(Uint8List data, int size) { + return Image.memory(data, + width: size.toDouble(), height: size.toDouble(), fit: BoxFit.cover); } } diff --git a/lib/utils/important_items_filter.dart b/lib/utils/important_items_filter.dart index 9fe3e29c4..8b4e52ccd 100644 --- a/lib/utils/important_items_filter.dart +++ b/lib/utils/important_items_filter.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:myapp/models/photo.dart'; import 'package:myapp/utils/gallery_items_filter.dart'; import 'package:path/path.dart'; @@ -5,11 +7,14 @@ import 'package:path/path.dart'; class ImportantItemsFilter implements GalleryItemsFilter { @override bool shouldInclude(Photo photo) { - // TODO: Improve logic - final String folder = basename(photo.relativePath); - return folder == "Camera" || - folder == "DCIM" || - folder == "Download" || - folder == "Screenshot"; + if (Platform.isAndroid) { + final String folder = basename(photo.pathName); + return folder == "Camera" || + folder == "DCIM" || + folder == "Download" || + folder == "Screenshot"; + } else { + return true; + } } } diff --git a/lib/utils/share_util.dart b/lib/utils/share_util.dart new file mode 100644 index 000000000..54f6ab23b --- /dev/null +++ b/lib/utils/share_util.dart @@ -0,0 +1,21 @@ +import 'dart:typed_data'; + +import 'package:esys_flutter_share/esys_flutter_share.dart'; +import 'package:myapp/models/photo.dart'; +import 'package:path/path.dart'; + +Future share(Photo photo) async { + final bytes = await photo.getBytes(); + final ext = extension(photo.title); + final shareExt = + ext == ".HEIC" ? "jpeg" : ext.substring(1, ext.length).toLowerCase(); + return Share.file(photo.title, photo.title, bytes, "image/" + shareExt); +} + +Future shareMultiple(List photos) async { + final shareContent = Map(); + for (Photo photo in photos) { + shareContent[photo.title] = await photo.getBytes(); + } + return Share.files("images", shareContent, "*/*"); +} diff --git a/pubspec.lock b/pubspec.lock index 08314ffe0..b23a43418 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.4" + esys_flutter_share: + dependency: "direct main" + description: + name: esys_flutter_share + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" flutter: dependency: "direct main" description: flutter @@ -233,13 +240,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" - share_extend: - dependency: "direct main" - description: - name: share_extend - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.3" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b36d0f395..98ecd4999 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: crypto: ^2.1.3 toast: ^0.1.5 image: ^2.1.4 - share_extend: "^1.1.2" + esys_flutter_share: ^1.0.2 draggable_scrollbar: ^0.0.4 photo_view: ^0.9.2 flutter_image_compress: ^0.6.5+1