*Break changes:

• move "newest" parameter from "listMedia" to "listAlbums" API
• move "newest" property from "MediaPage" to "Media"
• add "newest" parameter of getAlbumThumbnail API
• update album parameter of AlbumThumbnailProvider
This commit is contained in:
Wenqi Li 2023-05-13 15:49:58 +08:00
parent 7898c4bde3
commit 88e0ad714b
7 changed files with 183 additions and 90 deletions

View File

@ -138,12 +138,13 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
"getAlbumThumbnail" -> {
val albumId = call.argument<String>("albumId")
val mediumType = call.argument<String>("mediumType")
val newest = call.argument<Boolean>("newest")
val width = call.argument<Int>("width")
val height = call.argument<Int>("height")
val highQuality = call.argument<Boolean>("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<String>): 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<String>): 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
}
}

View File

@ -92,8 +92,7 @@ class _MyAppState extends State<MyApp> {
placeholder:
MemoryImage(kTransparentImage),
image: AlbumThumbnailProvider(
albumId: album.id,
mediumType: album.mediumType,
album: album,
highQuality: true,
),
),

View File

@ -55,12 +55,14 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin {
let arguments = call.arguments as! Dictionary<String, AnyObject>
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

View File

@ -23,26 +23,27 @@ class PhotoGallery {
/// List all available gallery albums and counts number of items of [MediumType].
static Future<List<Album>> 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<Album>((x) => Album.fromJson(x)).toList();
return json.map<Album>((album) => Album.fromJson(album, mediumType, newest)).toList();
}
/// List all available media in a specific album, support pagination of media
static Future<MediaPage> _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<List<int>?> 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,

View File

@ -3,15 +3,13 @@ part of photogallery;
/// Fetches the given album thumbnail from the gallery.
class AlbumThumbnailProvider extends ImageProvider<AlbumThumbnailProvider> {
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<AlbumThumbnailProvider> {
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<AlbumThumbnailProvider> {
Future<ui.Codec> _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<AlbumThumbnailProvider> {
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}")';
}

View File

@ -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 `<total>`).
Future<MediaPage> listMedia({
bool newest = true,
int? skip,
int? take,
}) {
return PhotoGallery._listMedia(
album: this,
newest: newest,
skip: skip,
take: take,
);

View File

@ -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<Medium>((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,
);