diff --git a/android/src/main/kotlin/com/morbit/photogallery/PhotoGalleryPlugin.kt b/android/src/main/kotlin/com/morbit/photogallery/PhotoGalleryPlugin.kt index f67970c..1a07af0 100644 --- a/android/src/main/kotlin/com/morbit/photogallery/PhotoGalleryPlugin.kt +++ b/android/src/main/kotlin/com/morbit/photogallery/PhotoGalleryPlugin.kt @@ -138,12 +138,13 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { "getAlbumThumbnail" -> { val albumId = call.argument("albumId") val mediumType = call.argument("mediumType") + val newest = call.argument("newest") val width = call.argument("width") val height = call.argument("height") val highQuality = call.argument("highQuality") executor.submit { result.success( - getAlbumThumbnail(albumId!!, mediumType, width, height, highQuality) + getAlbumThumbnail(albumId!!, mediumType, newest!!, width, height, highQuality) ) } } @@ -211,7 +212,6 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { val folderName = cursor.getString(bucketColumn) albumHashMap[bucketId] = hashMapOf( "id" to bucketId, - "mediumType" to imageType, "name" to folderName, "count" to 1 ) @@ -228,7 +228,6 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { allAlbumId, hashMapOf( "id" to allAlbumId, - "mediumType" to imageType, "name" to allAlbumName, "count" to total ) @@ -267,7 +266,6 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { val folderName = cursor.getString(bucketColumn) albumHashMap[bucketId] = hashMapOf( "id" to bucketId, - "mediumType" to videoType, "name" to folderName, "count" to 1 ) @@ -284,7 +282,6 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { allAlbumId, hashMapOf( "id" to allAlbumId, - "mediumType" to videoType, "name" to allAlbumName, "count" to total ) @@ -300,7 +297,6 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { val albumMap = (imageMap.keys + videoMap.keys).associateWith { mapOf( "id" to it, - "mediumType" to null, "name" to imageMap[it]?.get("name"), "count" to (imageMap[it]?.get("count") ?: 0) as Int + (videoMap[it]?.get("count") ?: 0) as Int, ) @@ -330,7 +326,6 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { items = items.subList(start, end) } mapOf( - "newest" to newest, "start" to (skip ?: 0), "items" to items ) @@ -390,7 +385,6 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } return mapOf( - "newest" to newest, "start" to skip, "items" to media ) @@ -448,7 +442,6 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } return mapOf( - "newest" to newest, "start" to skip, "items" to media ) @@ -596,55 +589,25 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { return byteArray } - private fun getAlbumThumbnail(albumId: String, mediumType: String?, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { + private fun getAlbumThumbnail(albumId: String, mediumType: String?, newest: Boolean, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { return when (mediumType) { imageType -> { - getImageAlbumThumbnail(albumId, width, height, highQuality) + getImageAlbumThumbnail(albumId, newest, width, height, highQuality) } videoType -> { - getVideoAlbumThumbnail(albumId, width, height, highQuality) + getVideoAlbumThumbnail(albumId, newest, width, height, highQuality) } else -> { - getImageAlbumThumbnail(albumId, width, height, highQuality) - ?: getVideoAlbumThumbnail(albumId, width, height, highQuality) + getAllAlbumThumbnail(albumId, newest, width, height, highQuality) } } } - private fun getImageAlbumThumbnail(albumId: String, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { + private fun getImageAlbumThumbnail(albumId: String, newest: Boolean, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { return this.context.run { - val isSelection = albumId != allAlbumId - val selection = if (isSelection) "${MediaStore.Images.Media.BUCKET_ID} = ?" else null - val selectionArgs = if (isSelection) arrayOf(albumId) else null - val orderBy = - "${MediaStore.Images.Media.DATE_ADDED} DESC, ${MediaStore.Images.Media.DATE_MODIFIED} DESC" + val projection = arrayOf(MediaStore.Images.Media._ID) - val imageCursor: Cursor? - - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - imageCursor = this.contentResolver.query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - arrayOf(MediaStore.Images.Media._ID), - android.os.Bundle().apply { - // Selection - putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection) - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs) - // Sort - putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, orderBy) - // Limit - putInt(ContentResolver.QUERY_ARG_LIMIT, 1) - }, - null - ) - } else { - imageCursor = this.contentResolver.query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - arrayOf(MediaStore.Images.Media._ID), - selection, - selectionArgs, - "$orderBy LIMIT 1" - ) - } + val imageCursor = getImageCursor(albumId, newest, projection) imageCursor?.use { cursor -> if (cursor.moveToFirst()) { @@ -658,20 +621,160 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } } - private fun getVideoAlbumThumbnail(albumId: String, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { + private fun getVideoAlbumThumbnail(albumId: String, newest: Boolean, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { return this.context.run { + val projection = arrayOf(MediaStore.Video.Media._ID) + + val videoCursor = getVideoCursor(albumId, newest, projection) + + videoCursor?.use { cursor -> + if (cursor.moveToNext()) { + val idColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID) + val id = cursor.getLong(idColumn) + return@run getVideoThumbnail(id.toString(), width, height, highQuality) + } + } + + return null + } + } + + private fun getAllAlbumThumbnail(albumId: String, newest: Boolean, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { + return this.context.run { + val imageProjection = arrayOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DATE_ADDED, + MediaStore.Images.Media.DATE_MODIFIED, + ) + + val imageCursor = getImageCursor(albumId, newest, imageProjection) + + var imageId: Long? = null + var imageDateAdded: Long? = null + var imageDateModified: Long? = null + imageCursor?.use { cursor -> + if (cursor.moveToFirst()) { + val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID) + val dateAddedColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED) + val dateModifiedColumn = + cursor.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED) + imageId = cursor.getLong(idColumn) + imageDateAdded = cursor.getLong(dateAddedColumn) * 1000 + imageDateModified = cursor.getLong(dateModifiedColumn) * 1000 + } + } + + val videoProjection = arrayOf( + MediaStore.Video.Media._ID, + MediaStore.Video.Media.DATE_ADDED, + MediaStore.Video.Media.DATE_MODIFIED, + ) + + val videoCursor = getVideoCursor(albumId, newest, videoProjection) + + var videoId: Long? = null + var videoDateAdded: Long? = null + var videoDateModified: Long? = null + videoCursor?.use { cursor -> + if (cursor.moveToFirst()) { + val idColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID) + val dateAddedColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATE_ADDED) + val dateModifiedColumn = + cursor.getColumnIndex(MediaStore.Video.Media.DATE_MODIFIED) + videoId = cursor.getLong(idColumn) + videoDateAdded = cursor.getLong(dateAddedColumn) * 1000 + videoDateModified = cursor.getLong(dateModifiedColumn) * 1000 + } + } + + if (imageId != null && videoId != null) { + if (imageDateAdded != null && videoDateAdded != null) { + if (newest && imageDateAdded!! < videoDateAdded!! || !newest && imageDateAdded!! > videoDateAdded!!) { + return@run getVideoThumbnail(videoId.toString(), width, height, highQuality) + } else { + return@run getImageThumbnail(imageId.toString(), width, height, highQuality) + } + } + if (imageDateModified != null && videoDateModified != null) { + if (newest && imageDateModified!! < videoDateModified!! || !newest && imageDateModified!! > videoDateModified!!) { + return@run getVideoThumbnail(videoId.toString(), width, height, highQuality) + } else { + return@run getImageThumbnail(imageId.toString(), width, height, highQuality) + } + } + } + + if (imageId != null) { + return@run getImageThumbnail(imageId.toString(), width, height, highQuality) + } + + if (videoId != null) { + return@run getVideoThumbnail(videoId.toString(), width, height, highQuality) + } + + return@run null + } + } + + private fun getImageCursor(albumId: String, newest: Boolean, projection: Array): Cursor? { + this.context.run { + val isSelection = albumId != allAlbumId + val selection = if (isSelection) "${MediaStore.Images.Media.BUCKET_ID} = ?" else null + val selectionArgs = if (isSelection) arrayOf(albumId) else null + val orderBy = if (newest) { + "${MediaStore.Images.Media.DATE_ADDED} DESC, ${MediaStore.Images.Media.DATE_MODIFIED} DESC" + } else { + "${MediaStore.Images.Media.DATE_ADDED} ASC, ${MediaStore.Images.Media.DATE_MODIFIED} ASC" + } + + val imageCursor: Cursor? + + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { + imageCursor = this.contentResolver.query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, + android.os.Bundle().apply { + // Selection + putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection) + putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs) + // Sort + putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, orderBy) + // Limit + putInt(ContentResolver.QUERY_ARG_LIMIT, 1) + }, + null + ) + } else { + imageCursor = this.contentResolver.query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, + selection, + selectionArgs, + "$orderBy LIMIT 1" + ) + } + + return imageCursor + } + } + + private fun getVideoCursor(albumId: String, newest: Boolean, projection: Array): Cursor? { + this.context.run { val isSelection = albumId != allAlbumId val selection = if (isSelection) "${MediaStore.Video.Media.BUCKET_ID} = ?" else null val selectionArgs = if (isSelection) arrayOf(albumId) else null - val orderBy = + val orderBy = if (newest) { "${MediaStore.Video.Media.DATE_ADDED} DESC, ${MediaStore.Video.Media.DATE_MODIFIED} DESC" + } else { + "${MediaStore.Video.Media.DATE_ADDED} ASC, ${MediaStore.Video.Media.DATE_MODIFIED} ASC" + } val videoCursor: Cursor? if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { videoCursor = this.contentResolver.query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, - arrayOf(MediaStore.Video.Media._ID), + projection, android.os.Bundle().apply { // Selection putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection) @@ -686,22 +789,14 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } else { videoCursor = this.contentResolver.query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, - arrayOf(MediaStore.Video.Media._ID), + projection, selection, selectionArgs, "$orderBy LIMIT 1" ) } - videoCursor?.use { cursor -> - if (cursor.moveToNext()) { - val idColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID) - val id = cursor.getLong(idColumn) - return@run getVideoThumbnail(id.toString(), width, height, highQuality) - } - } - - return null + return videoCursor } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 74dc9b8..417f0da 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -92,8 +92,7 @@ class _MyAppState extends State { placeholder: MemoryImage(kTransparentImage), image: AlbumThumbnailProvider( - albumId: album.id, - mediumType: album.mediumType, + album: album, highQuality: true, ), ), diff --git a/ios/Classes/SwiftPhotoGalleryPlugin.swift b/ios/Classes/SwiftPhotoGalleryPlugin.swift index a1c7c56..bcf2bde 100644 --- a/ios/Classes/SwiftPhotoGalleryPlugin.swift +++ b/ios/Classes/SwiftPhotoGalleryPlugin.swift @@ -55,12 +55,14 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { let arguments = call.arguments as! Dictionary let albumId = arguments["albumId"] as! String let mediumType = arguments["mediumType"] as? String + let newest = arguments["newest"] as! Bool let width = arguments["width"] as? Int let height = arguments["height"] as? Int let highQuality = arguments["highQuality"] as? Bool getAlbumThumbnail( albumId: albumId, mediumType: mediumType, + newest: newest, width: width, height: height, highQuality: highQuality, @@ -113,7 +115,6 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { self.assetCollections.append(collection) albums.append([ "id": collection.localIdentifier, - "mediumType": mediumType, "name": collection.localizedTitle ?? "Unknown", "count": count, ]) @@ -150,7 +151,6 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { albums.insert([ "id": "__ALL__", - "mediumType": mediumType, "name": "All", "count" : countMedia(collection: nil, mediumType: mediumType), ], at: 0) @@ -193,7 +193,6 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { } return [ - "newest": newest, "start": start, "items": items, ] @@ -265,6 +264,7 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { private func getAlbumThumbnail( albumId: String, mediumType: String?, + newest: Bool, width: Int?, height: Int?, highQuality: Bool?, @@ -274,8 +274,8 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { let fetchOptions = PHFetchOptions() fetchOptions.predicate = self.predicateFromMediumType(mediumType: mediumType) fetchOptions.sortDescriptors = [ - NSSortDescriptor(key: "creationDate", ascending: false), - NSSortDescriptor(key: "modificationDate", ascending: false) + NSSortDescriptor(key: "creationDate", ascending: !newest), + NSSortDescriptor(key: "modificationDate", ascending: !newest) ] if #available(iOS 9, *) { fetchOptions.fetchLimit = 1 diff --git a/lib/photo_gallery.dart b/lib/photo_gallery.dart index eb0788c..17db430 100644 --- a/lib/photo_gallery.dart +++ b/lib/photo_gallery.dart @@ -23,26 +23,27 @@ class PhotoGallery { /// List all available gallery albums and counts number of items of [MediumType]. static Future> listAlbums({ MediumType? mediumType, - bool? hideIfEmpty = true, + bool newest = true, + bool hideIfEmpty = true, }) async { final json = await _channel.invokeMethod('listAlbums', { 'mediumType': mediumTypeToJson(mediumType), + 'newest': newest, 'hideIfEmpty': hideIfEmpty, }); - return json.map((x) => Album.fromJson(x)).toList(); + return json.map((album) => Album.fromJson(album, mediumType, newest)).toList(); } /// List all available media in a specific album, support pagination of media static Future _listMedia({ required Album album, - bool newest = true, int? skip, int? take, }) async { final json = await _channel.invokeMethod('listMedia', { 'albumId': album.id, 'mediumType': mediumTypeToJson(album.mediumType), - 'newest': newest, + 'newest': album.newest, 'skip': skip, 'take': take, }); @@ -84,6 +85,7 @@ class PhotoGallery { static Future?> getAlbumThumbnail({ required String albumId, MediumType? mediumType, + bool newest = true, int? width, int? height, bool? highQuality = false, @@ -91,6 +93,7 @@ class PhotoGallery { final bytes = await _channel.invokeMethod('getAlbumThumbnail', { 'albumId': albumId, 'mediumType': mediumTypeToJson(mediumType), + 'newest': newest, 'width': width, 'height': height, 'highQuality': highQuality, diff --git a/lib/src/image_providers/album_thumbnail_provider.dart b/lib/src/image_providers/album_thumbnail_provider.dart index 60002f0..2f2e018 100644 --- a/lib/src/image_providers/album_thumbnail_provider.dart +++ b/lib/src/image_providers/album_thumbnail_provider.dart @@ -3,15 +3,13 @@ part of photogallery; /// Fetches the given album thumbnail from the gallery. class AlbumThumbnailProvider extends ImageProvider { const AlbumThumbnailProvider({ - required this.albumId, - this.mediumType, + required this.album, this.height, this.width, this.highQuality = false, }); - final String albumId; - final MediumType? mediumType; + final Album album; final int? height; final int? width; final bool? highQuality; @@ -22,7 +20,7 @@ class AlbumThumbnailProvider extends ImageProvider { codec: _loadAsync(key, decode), scale: 1.0, informationCollector: () sync* { - yield ErrorDescription('Id: $albumId'); + yield ErrorDescription('Id: ${album.id}'); }, ); } @@ -30,8 +28,9 @@ class AlbumThumbnailProvider extends ImageProvider { Future _loadAsync(AlbumThumbnailProvider key, DecoderBufferCallback decode) async { assert(key == this); final data = await PhotoGallery.getAlbumThumbnail( - albumId: albumId, - mediumType: mediumType, + albumId: album.id, + mediumType: album.mediumType, + newest: album.newest, height: height, width: width, highQuality: highQuality, @@ -54,12 +53,12 @@ class AlbumThumbnailProvider extends ImageProvider { bool operator ==(dynamic other) { if (other.runtimeType != runtimeType) return false; final AlbumThumbnailProvider typedOther = other; - return albumId == typedOther.albumId; + return album.id == typedOther.album.id; } @override - int get hashCode => albumId.hashCode; + int get hashCode => album.id.hashCode; @override - String toString() => '$runtimeType("$albumId")'; + String toString() => '$runtimeType("${album.id}")'; } diff --git a/lib/src/models/album.dart b/lib/src/models/album.dart index cddc9ee..d88cd94 100644 --- a/lib/src/models/album.dart +++ b/lib/src/models/album.dart @@ -9,6 +9,9 @@ class Album { /// The [MediumType] of the album. final MediumType? mediumType; + /// The sort direction is newest or not + final bool newest; + /// The name of the album. final String? name; @@ -19,9 +22,10 @@ class Album { bool get isAllAlbum => id == "__ALL__"; /// Creates a album from platform channel protocol. - Album.fromJson(dynamic json) + Album.fromJson(dynamic json, MediumType? mediumType, bool newest) : id = json['id'], - mediumType = jsonToMediumType(json['mediumType']), + mediumType = mediumType, + newest = newest, name = json['name'], count = json['count']; @@ -30,13 +34,11 @@ class Album { /// Pagination can be controlled out of [skip] (defaults to `0`) and /// [take] (defaults to ``). Future listMedia({ - bool newest = true, int? skip, int? take, }) { return PhotoGallery._listMedia( album: this, - newest: newest, skip: skip, take: take, ); diff --git a/lib/src/models/media_page.dart b/lib/src/models/media_page.dart index 1e49c94..22bf67f 100644 --- a/lib/src/models/media_page.dart +++ b/lib/src/models/media_page.dart @@ -5,9 +5,6 @@ part of photogallery; class MediaPage { final Album album; - /// The sort direction is newest or not - final bool newest; - /// The start offset for those media. final int start; @@ -22,8 +19,7 @@ class MediaPage { /// Creates a range of media from platform channel protocol. MediaPage.fromJson(this.album, dynamic json) - : newest = json['newest'], - start = json['start'], + : start = json['start'], items = json['items'].map((x) => Medium.fromJson(x)).toList(); /// Gets the next page of media in the album. @@ -31,7 +27,6 @@ class MediaPage { assert(!isLast); return PhotoGallery._listMedia( album: album, - newest: newest, skip: end, take: items.length, );