diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 00000000..d9be0c26 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,5 @@ +{ + "MD033": false, + "MD013": false, + "MD024": false +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bc189de..7877ebc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ that can be found in the LICENSE file. --> # CHANGELOG +## 2.6.0 + +Features: + +- Support `CustomFilter` for more filter options. (#901) +- Add two new static methods for `PhotoManager`: + - `getAssetCount` for getting assets count. + - `getAssetListRange` for getting assets between start and end. + ## 2.5.2 ### Improvements diff --git a/README.md b/README.md index 3b497c43..cb749a6b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ English | [中文说明](#) (🚧 WIP) [![Build status](https://img.shields.io/github/actions/workflow/status/fluttercandies/flutter_photo_manager/runnable.yml?branch=main&label=CI&logo=github&style=flat-square)](https://github.com/fluttercandies/flutter_photo_manager/actions/workflows/runnable.yml) [![GitHub license](https://img.shields.io/github/license/fluttercandies/flutter_photo_manager)](https://github.com/fluttercandies/flutter_photo_manager/blob/main/LICENSE) -[![GitHub stars](https://img.shields.io/github/stars/fluttercandies/flutter_photo_manager?logo=github&style=flat-square)](https://github.com/fluttercandies/flutter_photo_manager/stargazers) +[![GitHub stars](https://img.shields.io/github/stars/fluttercandies/flutter_photo_manager?style=social&label=Stars)](https://github.com/fluttercandies/flutter_photo_manager/stargazers) [![GitHub forks](https://img.shields.io/github/forks/fluttercandies/flutter_photo_manager?logo=github&style=flat-square)](https://github.com/fluttercandies/flutter_photo_manager/network) [![Awesome Flutter](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/Solido/awesome-flutter) FlutterCandies @@ -22,7 +22,7 @@ you can get assets (image/video/audio) on Android, iOS and macOS. ## Projects using this plugin | name | pub | github | -|:---------------------|:-------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| :------------------- | :----------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | wechat_assets_picker | [![pub package](https://img.shields.io/pub/v/wechat_assets_picker)](https://pub.dev/packages/wechat_assets_picker) | [![star](https://img.shields.io/github/stars/fluttercandies/flutter_wechat_assets_picker?style=social)](https://github.com/fluttercandies/flutter_wechat_assets_picker) | | wechat_camera_picker | [![pub package](https://img.shields.io/pub/v/wechat_camera_picker)](https://pub.dev/packages/wechat_camera_picker) | [![star](https://img.shields.io/github/stars/fluttercandies/flutter_wechat_camera_picker?style=social)](https://github.com/fluttercandies/flutter_wechat_camera_picker) | @@ -55,7 +55,11 @@ see the [migration guide](MIGRATION_GUIDE.md) for detailed info. * [Limited entities access on iOS](#limited-entities-access-on-ios) * [Get albums/folders (`AssetPathEntity`)](#get-albumsfolders--assetpathentity-) * [Get assets (`AssetEntity`)](#get-assets--assetentity-) + * [PMFilter](#pmfilter) + * [PMFilterOptionGroup](#pmfilteroptiongroup) + * [CustomFilter](#customfilter) * [From `AssetPathEntity`](#from-assetpathentity) + * [From `PhotoManager` (Since 2.6.0)](#from-photomanager--since-260-) * [From ID](#from-id) * [From raw data](#from-raw-data) * [From iCloud](#from-icloud) @@ -258,6 +262,142 @@ Assets (images/videos/audios) are abstracted as the [`AssetEntity`][] class. It represents a series of fields with `MediaStore` on Android, and the `PHAsset` object on iOS/macOS. + +#### PMFilter + +Some methods of `PhotoManager` and `AssetPathEntity` have a `filterOption`. + +- PhotoManager + - getAssetPathList (The filter param passed in by this method will be passed into the AssetPathEntity of the result) + - getAssetCount + - getAssetListRange + - getAssetListPaged +- AssetPathEntity + - constructor (Method not recommended for users) + - fromId + - obtainPathFromProperties (Method not recommended for users) + +The `PMFilter` have two implementations: +- [PMFilterOptionGroup](#PMFilterOptionGroup) +- [CustomFilter](#CustomFilter) + +##### PMFilterOptionGroup + +Before 2.6.0, the only way to implement it. + +```dart +final FilterOptionGroup filterOption = FilterOptionGroup( + imageOption: FilterOption( + sizeConstraint: SizeConstraint( + maxWidth: 10000, + maxHeight: 10000, + minWidth: 100, + minHeight: 100, + ignoreSize: false, + ), + ), + videoOption: FilterOption( + durationConstraint: DurationConstraint( + min: Duration(seconds: 1), + max: Duration(seconds: 30), + allowNullable: false, + ), + ), + createTimeCondition: DateTimeCondition( + min: DateTime(2020, 1, 1), + max: DateTime(2020, 12, 31), + ), + orders: [ + OrderOption( + type: OrderOptionType.createDate, + asc: false, + ), + ], + /// other options +); +``` + +##### CustomFilter + +This is an **experimental feature**, please submit an issue if you have any problems. + +`CustomFilter` was added in the 2.6.0 version of the plugin, +which provide more flexible filtering conditions against host platforms. + +It is closer to the native way of use. +You can customize where conditions and order conditions. +It is up to you to decide which fields to use for filtering and sorting + + +**Like sql** construct a sql statement. + +The column name of iOS or android is different, so you need to use the `CustomColumns.base`、`CustomColumns.android` or `CustomColumns.darwin` to get the column name. + +```dart +PMFilter createFilter() { + final CustomFilter filterOption = CustomFilter.sql( + where: '${CustomColumns.base.width} > 100 AND ${CustomColumns.base.height} > 200', + orderBy: [OrderByItem.desc(CustomColumns.base.createDate)], + ); + + return filterOption; +} +``` + +**Advanced** filter + +`class AdvancedCustomFilter extends CustomFilter` + +The `AdvancedCustomFilter` is a subclass of `CustomFilter`, The have builder methods to help make a filter. + +```dart + +PMFilter createFilter() { + final group = WhereConditionGroup() + .and( + ColumnWhereCondition( + column: CustomColumns.base.width, + value: '100', + operator: '>', + ), + ) + .or( + ColumnWhereCondition( + column: CustomColumns.base.height, + value: '200', + operator: '>', + ), + ); + + final filter = AdvancedCustomFilter() + .addWhereCondition(group) + .addOrderBy(column: CustomColumns.base.createDate, isAsc: false); + + return filter; +} + +``` + +**Main class** of custom filter + +- `CustomFilter` : The base class of custom filter. +- `OrderByItem` : The class of order by item. +- `SqlCustomFilter` : The subclass of `CustomFilter`, It is used to make a like sql filter. +- `AdvancedCustomFilter`: The subclass of `CustomFilter`, It is used to make a advanced filter. + - `WhereConditionItem` : The class of where condition item. + - `TextWhereCondition`: The class of where condition. The text will not be checked. + - `WhereConditionGroup` : The class of where condition group. The class is used to make a group of where condition. + - `ColumnWhereCondition`: The class of where condition. The column will be checked. + - `DateColumnWhereCondition`: The class of where condition. Because dates have different conversion methods on iOS/macOS, this implementation smoothes the platform differences +- `CustomColumns` : This class contains fields for different platforms. + - `base` : The common fields are included here, but please note that the "id" field is invalid in iOS and may even cause errors. It is only valid on Android. + - `android` : The columns of android. + - `darwin` : The columns of iOS/macOS. + +> PS: The CustomColumns should be noted that iOS uses the Photos API, while Android uses ContentProvider, which is closer to SQLite. Therefore, even though they are called "columns," these fields are PHAsset fields on iOS/macOS and MediaStoreColumns fields on Android. + +![flow_chart](flow_chart/advance_custom_filter.png) + #### From `AssetPathEntity` You can use [the pagination method][`getAssetListPaged`]: @@ -272,6 +412,29 @@ Or use [the range method][`getAssetListRange`]: final List entities = await path.getAssetListRange(start: 0, end: 80); ``` +#### From `PhotoManager` (Since 2.6.0) + +First, You need get count of assets: + +```dart +final int count = await PhotoManager.getAssetCount(); +``` + +Then, you can use [the pagination method][`getAssetListPaged`]: + +```dart +final List entities = await PhotoManager.getAssetListPaged(page: 0, pageCount: 80); +``` + +Or use [the range method][`getAssetListRange`]: + +```dart +final List entities = await PhotoManager.getAssetListRange(start: 0, end: 80); +``` + +**Note:** +The `page`, `start` is base 0. + #### From ID The ID concept represents: @@ -527,7 +690,7 @@ Here are caches generation on different platforms, types and resolutions. | Platform | Thumbnail | File / Origin File | -|----------|-----------|--------------------| +| -------- | --------- | ------------------ | | Android | Yes | No | | iOS | No | Yes | diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/constant/Methods.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/constant/Methods.kt index f692984a..62fd8adc 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/constant/Methods.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/constant/Methods.kt @@ -29,6 +29,10 @@ class Methods { const val copyAsset = "copyAsset" const val moveAssetToPath = "moveAssetToPath" const val removeNoExistsAssets = "removeNoExistsAssets" + const val getColumnNames = "getColumnNames" + + const val getAssetCount = "getAssetCount" + const val getAssetsByRange = "getAssetsByRange" /// Below methods have [RequestType] params, thus permissions are required for Android 13. const val fetchPathProperties = "fetchPathProperties" diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt index 2d4254f0..45e368b5 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt @@ -8,7 +8,7 @@ import android.util.Log import com.bumptech.glide.Glide import com.bumptech.glide.request.FutureTarget import com.fluttercandies.photo_manager.core.entity.AssetEntity -import com.fluttercandies.photo_manager.core.entity.FilterOption +import com.fluttercandies.photo_manager.core.entity.filter.FilterOption import com.fluttercandies.photo_manager.core.entity.AssetPathEntity import com.fluttercandies.photo_manager.core.entity.ThumbLoadOption import com.fluttercandies.photo_manager.core.utils.* @@ -276,4 +276,19 @@ class PhotoManager(private val context: Context) { Glide.with(context).clear(futureTarget) } } + + fun getColumnNames(resultHandler: ResultHandler) { + val columnNames = dbUtils.getColumnNames(context) + resultHandler.reply(columnNames) + } + + fun getAssetCount(resultHandler: ResultHandler, option: FilterOption, requestType: Int) { + val assetCount = dbUtils.getAssetCount(context, option, requestType) + resultHandler.reply(assetCount) + } + + fun getAssetsByRange(resultHandler: ResultHandler, option: FilterOption, start: Int, end: Int, requestType: Int) { + val list = dbUtils.getAssetsByRange(context, option, start, end, requestType) + resultHandler.reply(ConvertUtils.convertAssets(list)) + } } diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt index f18254ae..41dbc0cd 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt @@ -12,7 +12,7 @@ import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import com.fluttercandies.photo_manager.core.entity.AssetEntity -import com.fluttercandies.photo_manager.core.entity.FilterOption +import com.fluttercandies.photo_manager.core.entity.filter.FilterOption import com.fluttercandies.photo_manager.core.entity.PermissionResult import com.fluttercandies.photo_manager.core.entity.ThumbLoadOption import com.fluttercandies.photo_manager.core.utils.ConvertUtils @@ -185,7 +185,12 @@ class PhotoManagerPlugin( } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - permissionsUtils.addManifestWithPermission33(applicationContext, permissions, call, resultHandler) + permissionsUtils.addManifestWithPermission33( + applicationContext, + permissions, + call, + resultHandler + ) if (resultHandler.isReplied()) { return } @@ -233,252 +238,255 @@ class PhotoManagerPlugin( call: MethodCall, resultHandler: ResultHandler, needLocationPermission: Boolean + ) { + if (call.method == Methods.requestPermissionExtend) { + resultHandler.reply(PermissionResult.Authorized.value) + return + } + runOnBackground { + try { + handleMethodResult(call, resultHandler, needLocationPermission) + } catch (e: Exception) { + val method = call.method + val params = call.arguments + resultHandler.replyError( + "The $method method has an error: ${e.message}", + e.stackTraceToString(), + params + ) + } + } + } + + private fun handleMethodResult( + call: MethodCall, + resultHandler: ResultHandler, + needLocationPermission: Boolean ) { when (call.method) { - Methods.requestPermissionExtend -> resultHandler.reply(PermissionResult.Authorized.value) Methods.getAssetPathList -> { - runOnBackground { - val type = call.argument("type")!! - val hasAll = call.argument("hasAll")!! - val option = call.getOption() - val onlyAll = call.argument("onlyAll")!! + val type = call.argument("type")!! + val hasAll = call.argument("hasAll")!! + val option = call.getOption() + val onlyAll = call.argument("onlyAll")!! - val list = photoManager.getAssetPathList(type, hasAll, onlyAll, option) - resultHandler.reply(ConvertUtils.convertPaths(list)) - } + val list = photoManager.getAssetPathList(type, hasAll, onlyAll, option) + resultHandler.reply(ConvertUtils.convertPaths(list)) } Methods.getAssetListPaged -> { - runOnBackground { - val galleryId = call.argument("id")!! - val type = call.argument("type")!! - val page = call.argument("page")!! - val size = call.argument("size")!! - val option = call.getOption() - val list = photoManager.getAssetListPaged(galleryId, type, page, size, option) - resultHandler.reply(ConvertUtils.convertAssets(list)) - } + val galleryId = call.argument("id")!! + val type = call.argument("type")!! + val page = call.argument("page")!! + val size = call.argument("size")!! + val option = call.getOption() + val list = + photoManager.getAssetListPaged(galleryId, type, page, size, option) + resultHandler.reply(ConvertUtils.convertAssets(list)) } Methods.getAssetListRange -> { - runOnBackground { - val galleryId = call.getString("id") - val type = call.getInt("type") - val start = call.getInt("start") - val end = call.getInt("end") - val option = call.getOption() - val list: List = - photoManager.getAssetListRange(galleryId, type, start, end, option) - resultHandler.reply(ConvertUtils.convertAssets(list)) - } + val galleryId = call.getString("id") + val type = call.getInt("type") + val start = call.getInt("start") + val end = call.getInt("end") + val option = call.getOption() + val list: List = + photoManager.getAssetListRange(galleryId, type, start, end, option) + resultHandler.reply(ConvertUtils.convertAssets(list)) } Methods.getThumbnail -> { - runOnBackground { - val id = call.argument("id")!! - val optionMap = call.argument>("option")!! - val option = ThumbLoadOption.fromMap(optionMap) - photoManager.getThumb(id, option, resultHandler) - } + val id = call.argument("id")!! + val optionMap = call.argument>("option")!! + val option = ThumbLoadOption.fromMap(optionMap) + photoManager.getThumb(id, option, resultHandler) } Methods.requestCacheAssetsThumbnail -> { - runOnBackground { - val ids = call.argument>("ids")!! - val optionMap = call.argument>("option")!! - val option = ThumbLoadOption.fromMap(optionMap) - photoManager.requestCache(ids, option, resultHandler) - } + val ids = call.argument>("ids")!! + val optionMap = call.argument>("option")!! + val option = ThumbLoadOption.fromMap(optionMap) + photoManager.requestCache(ids, option, resultHandler) } Methods.cancelCacheRequests -> { - runOnBackground { - photoManager.cancelCacheRequests() - resultHandler.reply(null) - } + photoManager.cancelCacheRequests() + resultHandler.reply(null) } Methods.assetExists -> { - runOnBackground { - val id = call.argument("id")!! - photoManager.assetExists(id, resultHandler) - } + val id = call.argument("id")!! + photoManager.assetExists(id, resultHandler) } Methods.getFullFile -> { - runOnBackground { - val id = call.argument("id")!! - val isOrigin = if (!needLocationPermission) false else call.argument("isOrigin")!! - photoManager.getFile(id, isOrigin, resultHandler) - } + val id = call.argument("id")!! + val isOrigin = + if (!needLocationPermission) false else call.argument("isOrigin")!! + photoManager.getFile(id, isOrigin, resultHandler) } Methods.getOriginBytes -> { - runOnBackground { - val id = call.argument("id")!! - photoManager.getOriginBytes(id, resultHandler, needLocationPermission) - } + val id = call.argument("id")!! + photoManager.getOriginBytes(id, resultHandler, needLocationPermission) } Methods.getMediaUrl -> { - runOnBackground { - val id = call.argument("id")!! - val type = call.argument("type")!! - val mediaUri = photoManager.getMediaUri(id.toLong(), type) - resultHandler.reply(mediaUri) - } + val id = call.argument("id")!! + val type = call.argument("type")!! + val mediaUri = photoManager.getMediaUri(id.toLong(), type) + resultHandler.reply(mediaUri) } Methods.fetchEntityProperties -> { - runOnBackground { - val id = call.argument("id")!! - val asset = photoManager.fetchEntityProperties(id) - val assetResult = if (asset != null) { - ConvertUtils.convertAsset(asset) - } else { - null - } - resultHandler.reply(assetResult) + val id = call.argument("id")!! + val asset = photoManager.fetchEntityProperties(id) + val assetResult = if (asset != null) { + ConvertUtils.convertAsset(asset) + } else { + null } + resultHandler.reply(assetResult) } Methods.fetchPathProperties -> { - runOnBackground { - val id = call.argument("id")!! - val type = call.argument("type")!! - val option = call.getOption() - val pathEntity = photoManager.fetchPathProperties(id, type, option) - if (pathEntity != null) { - val mapResult = ConvertUtils.convertPaths(listOf(pathEntity)) - resultHandler.reply(mapResult) - } else { - resultHandler.reply(null) - } + val id = call.argument("id")!! + val type = call.argument("type")!! + val option = call.getOption() + val pathEntity = photoManager.fetchPathProperties(id, type, option) + if (pathEntity != null) { + val mapResult = ConvertUtils.convertPaths(listOf(pathEntity)) + resultHandler.reply(mapResult) + } else { + resultHandler.reply(null) } } Methods.getLatLng -> { - runOnBackground { - val id = call.argument("id")!! - // 读取id - val location = photoManager.getLocation(id) - resultHandler.reply(location) - } + val id = call.argument("id")!! + // 读取id + val location = photoManager.getLocation(id) + resultHandler.reply(location) } Methods.notify -> { - runOnBackground { - val notify = call.argument("notify") - if (notify == true) { - notifyChannel.startNotify() - } else { - notifyChannel.stopNotify() - } - resultHandler.reply(null) + val notify = call.argument("notify") + if (notify == true) { + notifyChannel.startNotify() + } else { + notifyChannel.stopNotify() } + resultHandler.reply(null) } Methods.saveImage -> { - runOnBackground { - try { - val image = call.argument("image")!! - val title = call.argument("title") ?: "" - val desc = call.argument("desc") ?: "" - val relativePath = call.argument("relativePath") ?: "" - val entity = photoManager.saveImage(image, title, desc, relativePath) - if (entity == null) { - resultHandler.reply(null) - return@runOnBackground - } - val map = ConvertUtils.convertAsset(entity) - resultHandler.reply(map) - } catch (e: Exception) { - LogUtils.error("save image error", e) + try { + val image = call.argument("image")!! + val title = call.argument("title") ?: "" + val desc = call.argument("desc") ?: "" + val relativePath = call.argument("relativePath") ?: "" + val entity = photoManager.saveImage(image, title, desc, relativePath) + if (entity == null) { resultHandler.reply(null) + return } + val map = ConvertUtils.convertAsset(entity) + resultHandler.reply(map) + } catch (e: Exception) { + LogUtils.error("save image error", e) + resultHandler.reply(null) } } Methods.saveImageWithPath -> { - runOnBackground { - try { - val imagePath = call.argument("path")!! - val title = call.argument("title") ?: "" - val desc = call.argument("desc") ?: "" - val relativePath = call.argument("relativePath") ?: "" - val entity = photoManager.saveImage(imagePath, title, desc, relativePath) - if (entity == null) { - resultHandler.reply(null) - return@runOnBackground - } - val map = ConvertUtils.convertAsset(entity) - resultHandler.reply(map) - } catch (e: Exception) { - LogUtils.error("save image error", e) + try { + val imagePath = call.argument("path")!! + val title = call.argument("title") ?: "" + val desc = call.argument("desc") ?: "" + val relativePath = call.argument("relativePath") ?: "" + val entity = + photoManager.saveImage(imagePath, title, desc, relativePath) + if (entity == null) { resultHandler.reply(null) + return } + val map = ConvertUtils.convertAsset(entity) + resultHandler.reply(map) + } catch (e: Exception) { + LogUtils.error("save image error", e) + resultHandler.reply(null) } } Methods.saveVideo -> { - runOnBackground { - try { - val videoPath = call.argument("path")!! - val title = call.argument("title")!! - val desc = call.argument("desc") ?: "" - val relativePath = call.argument("relativePath") ?: "" - val entity = photoManager.saveVideo(videoPath, title, desc, relativePath) - if (entity == null) { - resultHandler.reply(null) - return@runOnBackground - } - val map = ConvertUtils.convertAsset(entity) - resultHandler.reply(map) - } catch (e: Exception) { - LogUtils.error("save video error", e) + try { + val videoPath = call.argument("path")!! + val title = call.argument("title")!! + val desc = call.argument("desc") ?: "" + val relativePath = call.argument("relativePath") ?: "" + val entity = + photoManager.saveVideo(videoPath, title, desc, relativePath) + if (entity == null) { resultHandler.reply(null) + return } + val map = ConvertUtils.convertAsset(entity) + resultHandler.reply(map) + } catch (e: Exception) { + LogUtils.error("save video error", e) + resultHandler.reply(null) } } Methods.copyAsset -> { - runOnBackground { - val assetId = call.argument("assetId")!! - val galleryId = call.argument("galleryId")!! - photoManager.copyToGallery(assetId, galleryId, resultHandler) - } + val assetId = call.argument("assetId")!! + val galleryId = call.argument("galleryId")!! + photoManager.copyToGallery(assetId, galleryId, resultHandler) } Methods.moveAssetToPath -> { - runOnBackground { - val assetId = call.argument("assetId")!! - val albumId = call.argument("albumId")!! - photoManager.moveToGallery(assetId, albumId, resultHandler) - } + val assetId = call.argument("assetId")!! + val albumId = call.argument("albumId")!! + photoManager.moveToGallery(assetId, albumId, resultHandler) } Methods.deleteWithIds -> { - runOnBackground { - try { - val ids = call.argument>("ids")!! - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val uris = ids.map { photoManager.getUri(it) }.toList() - deleteManager.deleteInApi30(uris, resultHandler) - } else { - deleteManager.deleteInApi28(ids) - resultHandler.reply(ids) - } - } catch (e: Exception) { - LogUtils.error("deleteWithIds failed", e) - resultHandler.replyError("deleteWithIds failed") + try { + val ids = call.argument>("ids")!! + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val uris = ids.map { photoManager.getUri(it) }.toList() + deleteManager.deleteInApi30(uris, resultHandler) + } else { + deleteManager.deleteInApi28(ids) + resultHandler.reply(ids) } + } catch (e: Exception) { + LogUtils.error("deleteWithIds failed", e) + resultHandler.replyError("deleteWithIds failed") } } Methods.removeNoExistsAssets -> { - runOnBackground { - photoManager.removeAllExistsAssets(resultHandler) - } + photoManager.removeAllExistsAssets(resultHandler) + } + + Methods.getColumnNames -> { + photoManager.getColumnNames(resultHandler) + } + + Methods.getAssetCount -> { + val option = call.getOption() + val type = call.getInt("type") + photoManager.getAssetCount(resultHandler, option, type) + } + + Methods.getAssetsByRange -> { + val option = call.getOption() + val start = call.getInt("start") + val end = call.getInt("end") + val type = call.getInt("type") + photoManager.getAssetsByRange(resultHandler, option, start, end, type) } else -> resultHandler.notImplemented() diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/FilterOption.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/FilterOption.kt deleted file mode 100644 index 30c07ecd..00000000 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/FilterOption.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.fluttercandies.photo_manager.core.entity - -import android.annotation.SuppressLint -import android.provider.MediaStore -import com.fluttercandies.photo_manager.constant.AssetType -import com.fluttercandies.photo_manager.core.utils.ConvertUtils - -class FilterOption(map: Map<*, *>) { - val videoOption = ConvertUtils.getOptionFromType(map, AssetType.Video) - val imageOption = ConvertUtils.getOptionFromType(map, AssetType.Image) - val audioOption = ConvertUtils.getOptionFromType(map, AssetType.Audio) - val createDateCond = ConvertUtils.convertToDateCond(map["createDate"] as Map<*, *>) - val updateDateCond = ConvertUtils.convertToDateCond(map["updateDate"] as Map<*, *>) - val containsPathModified = map["containsPathModified"] as Boolean - - private val orderByCond: List = - ConvertUtils.convertToOrderByConds(map["orders"] as List<*>) - - fun orderByCondString(): String? { - if (orderByCond.isEmpty()) { - return null - } - return orderByCond.joinToString(",") { - it.getOrder() - } - } -} - -class FilterCond { - var isShowTitle = false - lateinit var sizeConstraint: SizeConstraint - lateinit var durationConstraint: DurationConstraint - - companion object { - private const val widthKey = MediaStore.Files.FileColumns.WIDTH - private const val heightKey = MediaStore.Files.FileColumns.HEIGHT - - @SuppressLint("InlinedApi") - private const val durationKey = MediaStore.Video.VideoColumns.DURATION - } - - fun sizeCond(): String = - "$widthKey >= ? AND $widthKey <= ? AND $heightKey >= ? AND $heightKey <=?" - - fun sizeArgs(): Array { - return arrayOf( - sizeConstraint.minWidth, - sizeConstraint.maxWidth, - sizeConstraint.minHeight, - sizeConstraint.maxHeight - ).toList().map { - it.toString() - }.toTypedArray() - } - - fun durationCond(): String { - val baseCond = "$durationKey >=? AND $durationKey <=?" - if (durationConstraint.allowNullable) { - return "( $durationKey IS NULL OR ( $baseCond ) )" - } - return baseCond - } - - fun durationArgs(): Array { - return arrayOf( - durationConstraint.min, - durationConstraint.max - ).map { it.toString() }.toTypedArray() - } - - class SizeConstraint { - var minWidth = 0 - var maxWidth = 0 - var minHeight = 0 - var maxHeight = 0 - var ignoreSize = false - } - - class DurationConstraint { - var min: Long = 0 - var max: Long = 0 - var allowNullable: Boolean = false - } -} - -data class DateCond( - val minMs: Long, - val maxMs: Long, - val ignore: Boolean -) - -data class OrderByCond( - val key: String, - val asc: Boolean -) { - fun getOrder(): String { - val ascValue = if (asc) { - "asc" - } else { - "desc" - } - return "$key $ascValue" - } -} diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/CommonFilterOption.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/CommonFilterOption.kt new file mode 100644 index 00000000..1c928ffd --- /dev/null +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/CommonFilterOption.kt @@ -0,0 +1,228 @@ +package com.fluttercandies.photo_manager.core.entity.filter + +import android.annotation.SuppressLint +import android.provider.MediaStore +import com.fluttercandies.photo_manager.constant.AssetType +import com.fluttercandies.photo_manager.core.utils.ConvertUtils +import com.fluttercandies.photo_manager.core.utils.RequestTypeUtils +import java.util.ArrayList + +class CommonFilterOption(map: Map<*, *>) : FilterOption() { + private val videoOption = ConvertUtils.getOptionFromType(map, AssetType.Video) + private val imageOption = ConvertUtils.getOptionFromType(map, AssetType.Image) + private val audioOption = ConvertUtils.getOptionFromType(map, AssetType.Audio) + private val createDateCond = ConvertUtils.convertToDateCond(map["createDate"] as Map<*, *>) + private val updateDateCond = ConvertUtils.convertToDateCond(map["updateDate"] as Map<*, *>) + override val containsPathModified = map["containsPathModified"] as Boolean + + private val orderByCond: List = + ConvertUtils.convertToOrderByConds(map["orders"] as List<*>) + + override fun makeWhere(requestType: Int, args: ArrayList, needAnd: Boolean): String { + val option = this + val typeSelection: String = getCondFromType(requestType, option, args) + val dateSelection = getDateCond(args, option) + val sizeWhere = sizeWhere(requestType, option) + return "$typeSelection $dateSelection $sizeWhere" + } + + override fun orderByCondString(): String? { + if (orderByCond.isEmpty()) { + return null + } + return orderByCond.joinToString(",") { + it.getOrder() + } + } + + private val typeUtils: RequestTypeUtils + get() = RequestTypeUtils + + /** + * Just filter [MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE] + */ + private fun sizeWhere(requestType: Int?, option: CommonFilterOption): String { + if (option.imageOption.sizeConstraint.ignoreSize) { + return "" + } + if (requestType == null || !typeUtils.containsImage(requestType)) { + return "" + } + val mediaType = MediaStore.Files.FileColumns.MEDIA_TYPE + var result = "" + if (typeUtils.containsVideo(requestType)) { + result = "OR ( $mediaType = ${MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO} )" + } + if (typeUtils.containsAudio(requestType)) { + result = "$result OR ( $mediaType = ${MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO} )" + } + val size = "${MediaStore.MediaColumns.WIDTH} > 0 AND ${MediaStore.MediaColumns.HEIGHT} > 0" + val imageCondString = + "( $mediaType = ${MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE} AND $size )" + result = "AND ($imageCondString $result)" + return result + } + + private fun getCondFromType( + type: Int, filterOption: CommonFilterOption, args: ArrayList + ): String { + val cond = StringBuilder() + val typeKey = MediaStore.Files.FileColumns.MEDIA_TYPE + + val haveImage = RequestTypeUtils.containsImage(type) + val haveVideo = RequestTypeUtils.containsVideo(type) + val haveAudio = RequestTypeUtils.containsAudio(type) + + var imageCondString = "" + var videoCondString = "" + var audioCondString = "" + + if (haveImage) { + val imageCond = filterOption.imageOption + imageCondString = "$typeKey = ? " + args.add(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString()) + if (!imageCond.sizeConstraint.ignoreSize) { + val sizeCond = imageCond.sizeCond() + val sizeArgs = imageCond.sizeArgs() + imageCondString = "$imageCondString AND $sizeCond" + args.addAll(sizeArgs) + } + } + + if (haveVideo) { + val videoCond = filterOption.videoOption + val durationCond = videoCond.durationCond() + val durationArgs = videoCond.durationArgs() + videoCondString = "$typeKey = ? AND $durationCond" + args.add(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()) + args.addAll(durationArgs) + } + + if (haveAudio) { + val audioCond = filterOption.audioOption + val durationCond = audioCond.durationCond() + val durationArgs = audioCond.durationArgs() + audioCondString = "$typeKey = ? AND $durationCond" + args.add(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO.toString()) + args.addAll(durationArgs) + } + + if (haveImage) { + cond.append("( $imageCondString )") + } + + if (haveVideo) { + if (cond.isNotEmpty()) { + cond.append("OR ") + } + cond.append("( $videoCondString )") + } + + if (haveAudio) { + if (cond.isNotEmpty()) { + cond.append("OR ") + } + cond.append("( $audioCondString )") + } + + return "AND ( $cond )" + } + + + private fun getDateCond(args: ArrayList, option: CommonFilterOption): String { + val createDateCond = + addDateCond(args, option.createDateCond, MediaStore.Images.Media.DATE_ADDED) + val updateDateCond = + addDateCond(args, option.updateDateCond, MediaStore.Images.Media.DATE_MODIFIED) + return "$createDateCond $updateDateCond" + } + + private fun addDateCond(args: ArrayList, dateCond: DateCond, dbKey: String): String { + if (dateCond.ignore) { + return "" + } + + val minMs = dateCond.minMs + val maxMs = dateCond.maxMs + + val dateSelection = "AND ( $dbKey >= ? AND $dbKey <= ? )" + args.add((minMs / 1000).toString()) + args.add((maxMs / 1000).toString()) + + return dateSelection + } + +} + +class FilterCond { + var isShowTitle = false + lateinit var sizeConstraint: SizeConstraint + lateinit var durationConstraint: DurationConstraint + + companion object { + private const val widthKey = MediaStore.Files.FileColumns.WIDTH + private const val heightKey = MediaStore.Files.FileColumns.HEIGHT + + @SuppressLint("InlinedApi") + private const val durationKey = MediaStore.Video.VideoColumns.DURATION + } + + fun sizeCond(): String = + "$widthKey >= ? AND $widthKey <= ? AND $heightKey >= ? AND $heightKey <=?" + + fun sizeArgs(): Array { + return arrayOf( + sizeConstraint.minWidth, + sizeConstraint.maxWidth, + sizeConstraint.minHeight, + sizeConstraint.maxHeight + ).toList().map { + it.toString() + }.toTypedArray() + } + + fun durationCond(): String { + val baseCond = "$durationKey >=? AND $durationKey <=?" + if (durationConstraint.allowNullable) { + return "( $durationKey IS NULL OR ( $baseCond ) )" + } + return baseCond + } + + fun durationArgs(): Array { + return arrayOf( + durationConstraint.min, durationConstraint.max + ).map { it.toString() }.toTypedArray() + } + + class SizeConstraint { + var minWidth = 0 + var maxWidth = 0 + var minHeight = 0 + var maxHeight = 0 + var ignoreSize = false + } + + class DurationConstraint { + var min: Long = 0 + var max: Long = 0 + var allowNullable: Boolean = false + } +} + +data class DateCond( + val minMs: Long, val maxMs: Long, val ignore: Boolean +) + +data class OrderByCond( + val key: String, val asc: Boolean +) { + fun getOrder(): String { + val ascValue = if (asc) { + "asc" + } else { + "desc" + } + return "$key $ascValue" + } +} diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/CustomOption.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/CustomOption.kt new file mode 100644 index 00000000..b3b1dfb6 --- /dev/null +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/CustomOption.kt @@ -0,0 +1,41 @@ +package com.fluttercandies.photo_manager.core.entity.filter + +import com.fluttercandies.photo_manager.core.utils.RequestTypeUtils +import java.util.ArrayList + +class CustomOption(private val map: Map<*, *>) : FilterOption() { + + override val containsPathModified: Boolean = map["containsPathModified"] as Boolean + + override fun orderByCondString(): String? { + val list = map["orderBy"] as? List<*> + if (list.isNullOrEmpty()) { + return null + } + return list.joinToString(",") { + val map = it as Map<*, *> + val column = map["column"] as String + val isAsc = map["isAsc"] as Boolean + "$column ${if (isAsc) "ASC" else "DESC"}" + } + } + + override fun makeWhere(requestType: Int, args: ArrayList, needAnd: Boolean): String { + val where = map["where"] as String + + val typeWhere = RequestTypeUtils.toWhere(requestType) + + if (where.trim().isEmpty()) { + if (needAnd) { + return "AND $typeWhere" + } + + return typeWhere + } + + if (needAnd && where.trim().isNotEmpty()) { + return "AND ( $where )" + } + return "( $where )" + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/FilterOption.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/FilterOption.kt new file mode 100644 index 00000000..8f2b6d01 --- /dev/null +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/filter/FilterOption.kt @@ -0,0 +1,11 @@ +package com.fluttercandies.photo_manager.core.entity.filter + +import java.util.ArrayList + +abstract class FilterOption { + abstract val containsPathModified: Boolean + + abstract fun orderByCondString(): String? + + abstract fun makeWhere(requestType: Int, args: ArrayList, needAnd: Boolean = true): String +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/AndroidQDBUtils.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/AndroidQDBUtils.kt index 278c265a..90c84f6f 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/AndroidQDBUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/AndroidQDBUtils.kt @@ -15,7 +15,7 @@ import androidx.exifinterface.media.ExifInterface import com.fluttercandies.photo_manager.core.PhotoManager import com.fluttercandies.photo_manager.core.cache.ScopedCache import com.fluttercandies.photo_manager.core.entity.AssetEntity -import com.fluttercandies.photo_manager.core.entity.FilterOption +import com.fluttercandies.photo_manager.core.entity.filter.FilterOption import com.fluttercandies.photo_manager.core.entity.AssetPathEntity import com.fluttercandies.photo_manager.util.LogUtils import java.io.ByteArrayOutputStream @@ -40,11 +40,9 @@ object AndroidQDBUtils : IDBUtils { ): List { val list = ArrayList() val args = ArrayList() - val typeSelection: String = getCondFromType(requestType, option, args) - val dateSelection = getDateCond(args, option) - val sizeWhere = sizeWhere(requestType, option) + val where = option.makeWhere(requestType, args) val selections = - "$BUCKET_ID IS NOT NULL $typeSelection $dateSelection $sizeWhere" + "$BUCKET_ID IS NOT NULL $where" val cursor = context.contentResolver.query( allUri, @@ -88,11 +86,9 @@ object AndroidQDBUtils : IDBUtils { ): List { val list = ArrayList() val args = ArrayList() - val typeSelection = getCondFromType(requestType, option, args) - val dateSelection = getDateCond(args, option) - val sizeWhere = sizeWhere(requestType, option) + val where = option.makeWhere(requestType, args) val selections = - "$BUCKET_ID IS NOT NULL $typeSelection $dateSelection $sizeWhere" + "$BUCKET_ID IS NOT NULL $where" val cursor = context.contentResolver.query( allUri, @@ -151,22 +147,19 @@ object AndroidQDBUtils : IDBUtils { if (!isAll) { args.add(pathId) } - val typeSelection: String = getCondFromType(requestType, option, args) - val sizeWhere = sizeWhere(requestType, option) - val dateSelection = getDateCond(args, option) - val keys = (assetKeys()).distinct().toTypedArray() + val where = option.makeWhere(requestType, args) val selection = if (isAll) { - "$BUCKET_ID IS NOT NULL $typeSelection $dateSelection $sizeWhere" + "$BUCKET_ID IS NOT NULL $where" } else { - "$BUCKET_ID = ? $typeSelection $dateSelection $sizeWhere" + "$BUCKET_ID = ? $where" } val sortOrder = getSortOrder(page * size, size, option) val cursor = context.contentResolver.query( - allUri, - keys, - selection, - args.toTypedArray(), - sortOrder + allUri, + keys(), + selection, + args.toTypedArray(), + sortOrder ) ?: return list cursor.use { cursorWithRange(it, page * size, size) { cursor -> @@ -193,20 +186,17 @@ object AndroidQDBUtils : IDBUtils { if (!isAll) { args.add(galleryId) } - val typeSelection: String = getCondFromType(requestType, option, args) - val sizeWhere = sizeWhere(requestType, option) - val dateSelection = getDateCond(args, option) - val keys = assetKeys().distinct().toTypedArray() + val where = option.makeWhere(requestType, args) val selection = if (isAll) { - "$BUCKET_ID IS NOT NULL $typeSelection $dateSelection $sizeWhere" + "$BUCKET_ID IS NOT NULL $where" } else { - "$BUCKET_ID = ? $typeSelection $dateSelection $sizeWhere" + "$BUCKET_ID = ? $where" } val pageSize = end - start val sortOrder = getSortOrder(start, pageSize, option) val cursor = context.contentResolver.query( allUri, - keys, + keys(), selection, args.toTypedArray(), sortOrder @@ -222,23 +212,26 @@ object AndroidQDBUtils : IDBUtils { } - private fun assetKeys() = - IDBUtils.storeImageKeys + IDBUtils.storeVideoKeys + IDBUtils.typeKeys + arrayOf(RELATIVE_PATH) + + override fun keys(): Array { + return (IDBUtils.storeImageKeys + IDBUtils.storeVideoKeys + IDBUtils.typeKeys + arrayOf( + RELATIVE_PATH + )).distinct().toTypedArray() + } override fun getAssetEntity( - context: Context, - id: String, - checkIfExists: Boolean + context: Context, + id: String, + checkIfExists: Boolean ): AssetEntity? { - val keys = assetKeys().distinct().toTypedArray() val selection = "$_ID = ?" val args = arrayOf(id) val cursor = context.contentResolver.query( - allUri, - keys, - selection, - args, - null + allUri, + keys(), + selection, + args, + null ) ?: return null cursor.use { return if (it.moveToNext()) it.toAssetEntity(context, checkIfExists) @@ -254,8 +247,9 @@ object AndroidQDBUtils : IDBUtils { ): AssetPathEntity? { val isAll = pathId == "" val args = ArrayList() - val typeSelection: String = getCondFromType(type, option, args) - val dateSelection = getDateCond(args, option) + + val where = option.makeWhere(type, args) + val idSelection: String if (isAll) { idSelection = "" @@ -263,9 +257,9 @@ object AndroidQDBUtils : IDBUtils { idSelection = "AND $BUCKET_ID = ?" args.add(pathId) } - val sizeWhere = sizeWhere(null, option) + val selection = - "$BUCKET_ID IS NOT NULL $typeSelection $dateSelection $idSelection $sizeWhere" + "$BUCKET_ID IS NOT NULL $where $idSelection" val cursor = context.contentResolver.query( allUri, IDBUtils.storeBucketKeys, diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/ConvertUtils.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/ConvertUtils.kt index 045695d8..fac3fb1c 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/ConvertUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/ConvertUtils.kt @@ -3,6 +3,7 @@ package com.fluttercandies.photo_manager.core.utils import android.provider.MediaStore import com.fluttercandies.photo_manager.constant.AssetType import com.fluttercandies.photo_manager.core.entity.* +import com.fluttercandies.photo_manager.core.entity.filter.* object ConvertUtils { fun convertPaths(list: List): Map { @@ -97,7 +98,14 @@ object ConvertUtils { } fun convertToFilterOptions(map: Map<*, *>): FilterOption { - return FilterOption(map) + val type = map["type"] as Int + val childMap = map["child"] as Map<*, *> + if (type == 0) { + return CommonFilterOption(childMap) + } else if (type == 1) { + return CustomOption(childMap) + } + throw IllegalStateException("Unknown type $type for filter option.") } fun convertToOrderByConds(orders: List<*>): List { diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt index 2c7629d1..7413f7fa 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt @@ -8,7 +8,7 @@ import android.util.Log import androidx.exifinterface.media.ExifInterface import com.fluttercandies.photo_manager.core.PhotoManager import com.fluttercandies.photo_manager.core.entity.AssetEntity -import com.fluttercandies.photo_manager.core.entity.FilterOption +import com.fluttercandies.photo_manager.core.entity.filter.FilterOption import com.fluttercandies.photo_manager.core.entity.AssetPathEntity import java.io.* import java.util.concurrent.locks.ReentrantLock @@ -18,22 +18,25 @@ import kotlin.concurrent.withLock @Suppress("Deprecation", "InlinedApi") object DBUtils : IDBUtils { private val locationKeys = arrayOf( - MediaStore.Images.ImageColumns.LONGITUDE, - MediaStore.Images.ImageColumns.LATITUDE + MediaStore.Images.ImageColumns.LONGITUDE, + MediaStore.Images.ImageColumns.LATITUDE ) + override fun keys(): Array = + (IDBUtils.storeImageKeys + IDBUtils.storeVideoKeys + IDBUtils.typeKeys + locationKeys).distinct() + .toTypedArray() + override fun getAssetPathList( - context: Context, - requestType: Int, - option: FilterOption + context: Context, + requestType: Int, + option: FilterOption ): List { val list = ArrayList() val args = ArrayList() - val typeSelection: String = getCondFromType(requestType, option, args) - val dateSelection = getDateCond(args, option) - val sizeWhere = sizeWhere(requestType, option) + val where = option.makeWhere(requestType, args) +// val where = makeWhere(requestType, option, args) val selection = - "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $typeSelection $dateSelection $sizeWhere) GROUP BY (${MediaStore.MediaColumns.BUCKET_ID}" + "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $where) GROUP BY (${MediaStore.MediaColumns.BUCKET_ID}" val cursor = context.contentResolver.query( allUri, IDBUtils.storeBucketKeys + arrayOf("count(1)"), @@ -56,19 +59,18 @@ object DBUtils : IDBUtils { return list } + override fun getMainAssetPathEntity( context: Context, requestType: Int, option: FilterOption ): List { val list = ArrayList() - val args = ArrayList() - val typeSelection: String = getCondFromType(requestType, option, args) val projection = IDBUtils.storeBucketKeys + arrayOf("count(1)") - val dateSelection = getDateCond(args, option) - val sizeWhere = sizeWhere(requestType, option) + val args = ArrayList() + val where = option.makeWhere(requestType, args) val selections = - "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $typeSelection $dateSelection $sizeWhere" + "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $where" val cursor = context.contentResolver.query( allUri, @@ -101,8 +103,6 @@ object DBUtils : IDBUtils { option: FilterOption ): AssetPathEntity? { val args = ArrayList() - val typeSelection: String = getCondFromType(type, option, args) - val dateSelection = getDateCond(args, option) val idSelection: String if (pathId == "") { idSelection = "" @@ -110,9 +110,9 @@ object DBUtils : IDBUtils { idSelection = "AND ${MediaStore.MediaColumns.BUCKET_ID} = ?" args.add(pathId) } - val sizeWhere = sizeWhere(null, option) + val where = option.makeWhere(type, args) val selection = - "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $typeSelection $dateSelection $idSelection $sizeWhere) GROUP BY (${MediaStore.MediaColumns.BUCKET_ID}" + "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $where $idSelection) GROUP BY (${MediaStore.MediaColumns.BUCKET_ID}" val cursor = context.contentResolver.query( allUri, IDBUtils.storeBucketKeys + arrayOf("count(1)"), @@ -146,15 +146,12 @@ object DBUtils : IDBUtils { if (!isAll) { args.add(pathId) } - val typeSelection = getCondFromType(requestType, option, args) - val dateSelection = getDateCond(args, option) - val sizeWhere = sizeWhere(requestType, option) - val keys = - (IDBUtils.storeImageKeys + IDBUtils.storeVideoKeys + IDBUtils.typeKeys + locationKeys).distinct().toTypedArray() + val where = option.makeWhere(requestType, args) + val keys = keys() val selection = if (isAll) { - "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $typeSelection $dateSelection $sizeWhere" + "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $where" } else { - "${MediaStore.MediaColumns.BUCKET_ID} = ? $typeSelection $dateSelection $sizeWhere" + "${MediaStore.MediaColumns.BUCKET_ID} = ? $where" } val sortOrder = getSortOrder(page * size, size, option) val cursor = context.contentResolver.query( @@ -188,21 +185,19 @@ object DBUtils : IDBUtils { if (!isAll) { args.add(galleryId) } - val typeSelection = getCondFromType(requestType, option, args) - val dateSelection = getDateCond(args, option) - val sizeWhere = sizeWhere(requestType, option) - val keys = - (IDBUtils.storeImageKeys + IDBUtils.storeVideoKeys + IDBUtils.typeKeys + locationKeys).distinct().toTypedArray() + val where = option.makeWhere(requestType, args) + val keys = keys() + val selection = if (isAll) { - "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $typeSelection $dateSelection $sizeWhere" + "${MediaStore.MediaColumns.BUCKET_ID} IS NOT NULL $where" } else { - "${MediaStore.MediaColumns.BUCKET_ID} = ? $typeSelection $dateSelection $sizeWhere" + "${MediaStore.MediaColumns.BUCKET_ID} = ? $where" } val pageSize = end - start val sortOrder = getSortOrder(start, pageSize, option) val cursor = context.contentResolver.query( - allUri, - keys, + allUri, + keys, selection, args.toTypedArray(), sortOrder @@ -217,9 +212,14 @@ object DBUtils : IDBUtils { return list } - override fun getAssetEntity(context: Context, id: String, checkIfExists: Boolean): AssetEntity? { + override fun getAssetEntity( + context: Context, + id: String, + checkIfExists: Boolean + ): AssetEntity? { val keys = - (IDBUtils.storeImageKeys + IDBUtils.storeVideoKeys + locationKeys + IDBUtils.typeKeys).distinct().toTypedArray() + (IDBUtils.storeImageKeys + IDBUtils.storeVideoKeys + locationKeys + IDBUtils.typeKeys).distinct() + .toTypedArray() val selection = "${MediaStore.MediaColumns._ID} = ?" val args = arrayOf(id) diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt index 624e29dd..d07a190d 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt @@ -16,11 +16,13 @@ import androidx.annotation.ChecksSdkIntAtLeast import androidx.exifinterface.media.ExifInterface import com.fluttercandies.photo_manager.core.PhotoManager import com.fluttercandies.photo_manager.core.entity.AssetEntity -import com.fluttercandies.photo_manager.core.entity.DateCond -import com.fluttercandies.photo_manager.core.entity.FilterOption import com.fluttercandies.photo_manager.core.entity.AssetPathEntity +import com.fluttercandies.photo_manager.core.entity.filter.FilterOption import com.fluttercandies.photo_manager.util.LogUtils -import java.io.* +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileInputStream +import java.io.InputStream import java.net.URLConnection @Suppress("Deprecation", "InlinedApi", "Range") @@ -66,29 +68,29 @@ interface IDBUtils { } val typeKeys = arrayOf( - MediaStore.Files.FileColumns.MEDIA_TYPE, - MediaStore.Images.Media.DISPLAY_NAME + MediaStore.Files.FileColumns.MEDIA_TYPE, + MediaStore.Images.Media.DISPLAY_NAME ) val storeBucketKeys = arrayOf(BUCKET_ID, BUCKET_DISPLAY_NAME) val allUri: Uri get() = MediaStore.Files.getContentUri(VOLUME_EXTERNAL) + } + fun keys(): Array + val idSelection: String get() = "${MediaStore.Images.Media._ID} = ?" val allUri: Uri get() = IDBUtils.allUri - private val typeUtils: RequestTypeUtils - get() = RequestTypeUtils - fun getAssetPathList( - context: Context, - requestType: Int = 0, - option: FilterOption + context: Context, + requestType: Int = 0, + option: FilterOption ): List fun getAssetListPaged( @@ -486,93 +488,6 @@ interface IDBUtils { needLocationPermission: Boolean ): ByteArray - /** - * Just filter [MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE] - */ - fun sizeWhere(requestType: Int?, option: FilterOption): String { - if (option.imageOption.sizeConstraint.ignoreSize) { - return "" - } - if (requestType == null || !typeUtils.containsImage(requestType)) { - return "" - } - val mediaType = MediaStore.Files.FileColumns.MEDIA_TYPE - var result = "" - if (typeUtils.containsVideo(requestType)) { - result = "OR ( $mediaType = ${MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO} )" - } - if (typeUtils.containsAudio(requestType)) { - result = "$result OR ( $mediaType = ${MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO} )" - } - val size = "$WIDTH > 0 AND $HEIGHT > 0" - val imageCondString = "( $mediaType = ${MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE} AND $size )" - result = "AND ($imageCondString $result)" - return result - } - - fun getCondFromType(type: Int, filterOption: FilterOption, args: ArrayList): String { - val cond = StringBuilder() - val typeKey = MediaStore.Files.FileColumns.MEDIA_TYPE - - val haveImage = RequestTypeUtils.containsImage(type) - val haveVideo = RequestTypeUtils.containsVideo(type) - val haveAudio = RequestTypeUtils.containsAudio(type) - - var imageCondString = "" - var videoCondString = "" - var audioCondString = "" - - if (haveImage) { - val imageCond = filterOption.imageOption - imageCondString = "$typeKey = ? " - args.add(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString()) - if (!imageCond.sizeConstraint.ignoreSize) { - val sizeCond = imageCond.sizeCond() - val sizeArgs = imageCond.sizeArgs() - imageCondString = "$imageCondString AND $sizeCond" - args.addAll(sizeArgs) - } - } - - if (haveVideo) { - val videoCond = filterOption.videoOption - val durationCond = videoCond.durationCond() - val durationArgs = videoCond.durationArgs() - videoCondString = "$typeKey = ? AND $durationCond" - args.add(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()) - args.addAll(durationArgs) - } - - if (haveAudio) { - val audioCond = filterOption.audioOption - val durationCond = audioCond.durationCond() - val durationArgs = audioCond.durationArgs() - audioCondString = "$typeKey = ? AND $durationCond" - args.add(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO.toString()) - args.addAll(durationArgs) - } - - if (haveImage) { - cond.append("( $imageCondString )") - } - - if (haveVideo) { - if (cond.isNotEmpty()) { - cond.append("OR ") - } - cond.append("( $videoCondString )") - } - - if (haveAudio) { - if (cond.isNotEmpty()) { - cond.append("OR ") - } - cond.append("( $audioCondString )") - } - - return "AND ( $cond )" - } - fun logRowWithId(context: Context, id: String) { if (LogUtils.isLog) { val splitter = "".padStart(40, '-') @@ -601,29 +516,6 @@ interface IDBUtils { option: FilterOption ): List - fun getDateCond(args: ArrayList, option: FilterOption): String { - val createDateCond = - addDateCond(args, option.createDateCond, MediaStore.Images.Media.DATE_ADDED) - val updateDateCond = - addDateCond(args, option.updateDateCond, MediaStore.Images.Media.DATE_MODIFIED) - return "$createDateCond $updateDateCond" - } - - private fun addDateCond(args: ArrayList, dateCond: DateCond, dbKey: String): String { - if (dateCond.ignore) { - return "" - } - - val minMs = dateCond.minMs - val maxMs = dateCond.maxMs - - val dateSelection = "AND ( $dbKey >= ? AND $dbKey <= ? )" - args.add((minMs / 1000).toString()) - args.add((maxMs / 1000).toString()) - - return dateSelection - } - fun getSortOrder(start: Int, pageSize: Int, filterOption: FilterOption): String? { val orderBy = filterOption.orderByCondString() return "$orderBy LIMIT $pageSize OFFSET $start" @@ -733,4 +625,43 @@ interface IDBUtils { } return null } + + fun getColumnNames(context: Context): List { + val cr = context.contentResolver + cr.query(allUri, null, null, null, null)?.use { + return it.columnNames.toList() + } + return emptyList() + } + + fun getAssetCount(context: Context, option: FilterOption, requestType: Int): Int { + val cr = context.contentResolver + val args = ArrayList() + val where = option.makeWhere(requestType, args, false) + val order = option.orderByCondString() + cr.query(allUri, arrayOf(_ID), where, args.toTypedArray(), order).use { + return it?.count ?: 0 + } + } + + fun getAssetsByRange(context: Context, option: FilterOption, start: Int, end: Int, requestType: Int): List { + val cr = context.contentResolver + val args = ArrayList() + val where = option.makeWhere(requestType, args, false) + val order = option.orderByCondString() + cr.query(allUri, keys(), where, args.toTypedArray(), order)?.use { + val result = ArrayList() + it.moveToPosition(start - 1) + while (it.moveToNext()) { + val asset = it.toAssetEntity(context, false) ?: continue + result.add(asset) + + if (result.count() == end - start) { + break + } + } + + return result + } ?: return emptyList() + } } diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/RequestTypeUtils.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/RequestTypeUtils.kt index faebb4a9..7d23546a 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/RequestTypeUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/RequestTypeUtils.kt @@ -1,5 +1,7 @@ package com.fluttercandies.photo_manager.core.utils +import android.provider.MediaStore + object RequestTypeUtils { private const val typeImage = 1 private const val typeVideo = 1.shl(1) @@ -14,4 +16,23 @@ object RequestTypeUtils { private fun checkType(type: Int, targetType: Int): Boolean { return type and targetType == targetType } + + fun toWhere(requestType: Int): String { + val typeInt = arrayListOf() + if (containsImage(requestType)) { + typeInt.add(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) + } + if (containsAudio(requestType)) { + typeInt.add(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO) + } + if (containsVideo(requestType)) { + typeInt.add(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) + } + + val where = typeInt.joinToString(" OR ") { + "${MediaStore.Files.FileColumns.MEDIA_TYPE} = $it" + } + + return "( $where )" + } } diff --git a/example/android/build.gradle b/example/android/build.gradle index 1f0db819..bb1f9da3 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.21' + ext.kotlin_version = '1.7.22' repositories { google() mavenCentral() diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 8bd70f35..00e33ede 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip diff --git a/example/custom.dart b/example/custom.dart new file mode 100644 index 00000000..de54261d --- /dev/null +++ b/example/custom.dart @@ -0,0 +1,19 @@ +import 'package:photo_manager/photo_manager.dart'; + +void main(List args) { + final filter = AdvancedCustomFilter().addWhereCondition( + WhereConditionGroup() + .andGroup( + WhereConditionGroup() + .andText('width > 1000') + .andText('height > 1000'), + ) + .orGroup( + WhereConditionGroup().andText('width < 500').andText('height < 500'), + ), + ); + + PhotoManager.getAssetPathList(filterOption: filter).then((value) { + print(value); + }); +} diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f9..9625e105 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 814a7248..fb9d86a0 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 6dc37544..4e3e4a2a 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -339,7 +339,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -355,10 +355,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 64MAFNBS88; + DEVELOPMENT_TEAM = S5GU4EMC47; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -418,7 +418,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -467,7 +467,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -485,10 +485,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 64MAFNBS88; + DEVELOPMENT_TEAM = S5GU4EMC47; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -509,10 +509,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 64MAFNBS88; + DEVELOPMENT_TEAM = S5GU4EMC47; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3db53b6e..c87d15a3 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,8 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + - - .value( - value: provider, // This is for the advanced usages. - child: const MaterialApp( - title: 'Photo Manager Example', - home: _SimpleExamplePage(), - ), + return ChangeNotifierProvider.value( + value: provider, // This is for the advanced usages. + child: MaterialApp( + title: 'Photo Manager Example', + builder: (context, child) { + if (child == null) return const SizedBox.shrink(); + return Banner( + message: 'Debug', + location: BannerLocation.bottomStart, + child: OKToast(child: child), + ); + }, + home: const _SimpleExamplePage(), + debugShowCheckedModeBanner: false, ), ); } diff --git a/example/lib/page/custom_filter/advance_filter_page.dart b/example/lib/page/custom_filter/advance_filter_page.dart new file mode 100644 index 00000000..2590bd2a --- /dev/null +++ b/example/lib/page/custom_filter/advance_filter_page.dart @@ -0,0 +1,371 @@ +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:photo_manager_example/page/custom_filter/order_by_action.dart'; + +class AdvancedCustomFilterPage extends StatefulWidget { + const AdvancedCustomFilterPage({ + Key? key, + required this.builder, + }) : super(key: key); + + final Widget Function(BuildContext context, CustomFilter filter) builder; + + @override + State createState() => + _AdvancedCustomFilterPageState(); +} + +class _AdvancedCustomFilterPageState extends State { + final List _orderBy = [ + OrderByItem.named( + column: CustomColumns.base.createDate, + isAsc: false, + ), + ]; + + final List _where = []; + + late CustomFilter filter; + + @override + void initState() { + super.initState(); + filter = _createFilter(); + } + + AdvancedCustomFilter _createFilter() { + final filter = AdvancedCustomFilter( + orderBy: _orderBy, + where: _where, + ); + return filter; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Advanced Custom Filter Example'), + actions: [ + WhereAction( + where: _where, + onChanged: (value) { + if (!mounted) return; + setState(() { + _where.clear(); + _where.addAll(value); + }); + }, + ), + OrderByAction( + items: _orderBy, + onChanged: (values) { + if (!mounted) return; + setState(() { + _orderBy.clear(); + _orderBy.addAll(values); + }); + }, + ), + ], + ), + body: Column( + children: [ + Expanded( + child: widget.builder(context, filter), + ), + ], + ), + ); + } +} + +class WhereAction extends StatelessWidget { + const WhereAction({ + Key? key, + required this.where, + required this.onChanged, + // required this + }) : super(key: key); + + final List where; + final ValueChanged> onChanged; + + @override + Widget build(BuildContext context) { + return IconButton( + icon: const Icon(Icons.filter_alt), + onPressed: () { + Navigator.push>( + context, + MaterialPageRoute( + builder: (context) => _WhereConditionPage(where: where), + ), + ).then((value) { + if (value != null) { + onChanged(value); + } + }); + }, + ); + } +} + +class _WhereConditionPage extends StatefulWidget { + const _WhereConditionPage({ + Key? key, + required this.where, + }) : super(key: key); + + final List where; + + @override + State<_WhereConditionPage> createState() => _WhereConditionPageState(); +} + +class _WhereConditionPageState extends State<_WhereConditionPage> { + final List _where = []; + + bool isChanged = false; + + @override + void initState() { + super.initState(); + _where.addAll(widget.where); + } + + Future _onWillPop() { + if (!isChanged) { + return Future.value(true); + } + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Are you sure?'), + content: const Text('Do you want to leave without saving?'), + actions: [ + TextButton( + child: const Text('No'), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + TextButton( + child: const Text('Yes'), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ); + }, + ).then((value) => value == true); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: _onWillPop, + child: Scaffold( + appBar: AppBar( + title: const Text('Where Condition'), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: _createNew, + ), + ], + ), + body: buildList(context), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.done), + onPressed: () { + Navigator.of(context).pop(_where); + }, + ), + ), + ); + } + + void _createNew() async { + final result = await showDialog( + context: context, + builder: (context) { + return const _CreateWhereDialog(); + }, + ); + if (result != null) { + setState(() { + isChanged = true; + _where.add(result); + }); + } + } + + Widget buildList(BuildContext context) { + return ListView.builder( + itemCount: _where.length, + itemBuilder: (context, index) { + final item = _where[index]; + return ListTile( + title: Text(item.display()), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + setState(() { + isChanged = true; + _where.removeAt(index); + }); + }, + ), + ); + }, + ); + } +} + +class _CreateWhereDialog extends StatefulWidget { + const _CreateWhereDialog({Key? key}) : super(key: key); + + @override + State<_CreateWhereDialog> createState() => _CreateWhereDialogState(); +} + +class _CreateWhereDialogState extends State<_CreateWhereDialog> { + List keys() { + return CustomColumns.platformValues(); + } + + late String column = keys().first; + String condition = '=='; + TextEditingController textValueController = TextEditingController(); + + var _date = DateTime.now(); + + WhereConditionItem createItem() { + final cond = condition; + + if (isDateColumn()) { + return DateColumnWhereCondition( + column: column, + operator: condition, + value: _date, + ); + } + + final value = '$column $cond ${textValueController.text}'; + final item = WhereConditionItem.text(value); + return item; + } + + bool isDateColumn() { + final dateColumns = CustomColumns.dateColumns(); + return dateColumns.contains(column); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Create Where Condition'), + content: Container( + width: double.maxFinite, + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DropdownButton( + items: keys().map((e) { + return DropdownMenuItem( + value: e, + child: Text(e), + ); + }).toList(), + onChanged: (value) { + if (value == null) return; + setState(() { + column = value; + }); + }, + value: column, + ), + DropdownButton( + hint: const Text('Condition'), + items: WhereConditionItem.platformConditions.map((e) { + return DropdownMenuItem( + value: e, + child: Text(e), + ); + }).toList(), + onChanged: (value) { + if (value == null) return; + setState(() { + condition = value; + }); + }, + value: condition, + ), + if (!isDateColumn()) + TextField( + controller: textValueController, + decoration: const InputDecoration( + hintText: 'Input condition', + ), + onChanged: (value) { + setState(() {}); + }, + ) + else + _datePicker(), + const SizedBox( + height: 16, + ), + Text( + createItem().text, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 20, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(createItem()); + }, + child: const Text('OK'), + ), + ], + ); + } + + Widget _datePicker() { + return Column( + children: [ + TextButton( + onPressed: () async { + final date = await showDatePicker( + context: context, + initialDate: _date, + firstDate: DateTime(1970), + lastDate: DateTime(2100), + ); + if (date == null) return; + setState(() { + _date = date; + }); + }, + child: Text(_date.toIso8601String()), + ), + ], + ); + } +} diff --git a/example/lib/page/custom_filter/custom_filter_sql_page.dart b/example/lib/page/custom_filter/custom_filter_sql_page.dart new file mode 100644 index 00000000..249aeaad --- /dev/null +++ b/example/lib/page/custom_filter/custom_filter_sql_page.dart @@ -0,0 +1,116 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; + +import 'order_by_action.dart'; + +class CustomFilterSqlPage extends StatefulWidget { + const CustomFilterSqlPage({ + Key? key, + required this.builder, + }) : super(key: key); + + final Widget Function(BuildContext context, CustomFilter filter) builder; + + @override + State createState() => _CustomFilterSqlPageState(); +} + +class _CustomFilterSqlPageState extends State { + final TextEditingController _whereController = TextEditingController(); + final List _orderBy = [ + OrderByItem.named( + column: CustomColumns.base.createDate, + isAsc: false, + ), + ]; + + late CustomFilter filter; + + @override + void initState() { + super.initState(); + if (Platform.isAndroid) { + const columns = CustomColumns.android; + _whereController.text = + '(${columns.width} is not null) OR ${columns.width} >= 250'; + } else if (Platform.isIOS || Platform.isMacOS) { + const columns = CustomColumns.darwin; + _whereController.text = + '${columns.width} <= 1000 AND ${columns.width} >= 250'; + } + + filter = createCustomFilter(); + } + + @override + void dispose() { + _whereController.dispose(); + super.dispose(); + } + + void refresh() { + setState(() { + filter = createCustomFilter(); + }); + } + + CustomFilter createCustomFilter() { + final filter = CustomFilter.sql( + where: _whereController.text, + orderBy: _orderBy, + ); + return filter; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Custom Filter'), + actions: [ + OrderByAction( + items: _orderBy, + onChanged: (List value) { + _orderBy.clear(); + _orderBy.addAll(value); + refresh(); + }, + ), + ], + ), + body: Column( + children: [ + TextField( + controller: _whereController, + decoration: const InputDecoration( + labelText: 'Where', + ), + onSubmitted: (value) { + refresh(); + }, + onEditingComplete: () { + refresh(); + }, + ), + ListTile( + title: Text( + 'Order By: \n${_orderBy.map((e) => e.toString()).join('\n')}'), + subtitle: const Text('Click to edit'), + onTap: () { + changeOrderBy(context, _orderBy, (List value) { + _orderBy.clear(); + _orderBy.addAll(value); + refresh(); + }); + }, + ), + Expanded( + child: widget.builder(context, filter), + ), + ], + ), + ); + } +} diff --git a/example/lib/page/custom_filter/filter_assets_page.dart b/example/lib/page/custom_filter/filter_assets_page.dart new file mode 100644 index 00000000..ca523b91 --- /dev/null +++ b/example/lib/page/custom_filter/filter_assets_page.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:photo_manager_example/page/custom_filter/image_list.dart'; + +class FilterAssetsContent extends StatelessWidget { + const FilterAssetsContent({ + Key? key, + required this.filter, + }) : super(key: key); + final CustomFilter filter; + + Future> getAssets() async { + final count = await PhotoManager.getAssetCount(filterOption: filter); + if (count == 0) { + return []; + } + final list = await PhotoManager.getAssetListRange( + start: 0, + end: count, + filterOption: filter, + ); + return list; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: getAssets(), + builder: + (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return ImageList(list: snapshot.data!); + } + return const SizedBox(); + }, + ); + } +} diff --git a/example/lib/page/custom_filter/image_list.dart b/example/lib/page/custom_filter/image_list.dart new file mode 100644 index 00000000..6c811cbf --- /dev/null +++ b/example/lib/page/custom_filter/image_list.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:photo_manager_example/widget/image_item_widget.dart'; + +class ImageList extends StatelessWidget { + const ImageList({Key? key, required this.list}) : super(key: key); + + final List list; + + @override + Widget build(BuildContext context) { + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + mainAxisSpacing: 2, + crossAxisSpacing: 2, + ), + itemBuilder: (context, index) { + final entity = list[index]; + return ImageItemWidget( + entity: entity, + option: ThumbnailOption.ios( + size: const ThumbnailSize.square(500), + ), + ); + }, + itemCount: list.length, + ); + } +} diff --git a/example/lib/page/custom_filter/order_by_action.dart b/example/lib/page/custom_filter/order_by_action.dart new file mode 100644 index 00000000..77213c7f --- /dev/null +++ b/example/lib/page/custom_filter/order_by_action.dart @@ -0,0 +1,261 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; + +Future changeOrderBy( + BuildContext context, + List items, + ValueChanged> onChanged, +) async { + final result = await Navigator.push>( + context, + MaterialPageRoute>( + builder: (_) => OrderByActionPage( + items: items.toList(), + ), + ), + ); + if (result != null) { + onChanged(result); + } +} + +class OrderByAction extends StatelessWidget { + const OrderByAction({ + Key? key, + required this.items, + required this.onChanged, + }) : super(key: key); + + final List items; + final ValueChanged> onChanged; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Align( + alignment: Alignment.center, + child: IconButton( + onPressed: () async { + await changeOrderBy(context, items, onChanged); + }, + icon: const Icon(Icons.sort), + ), + ), + Positioned( + right: 5, + top: 5, + child: Container( + width: 15, + height: 15, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + items.length.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 8, + ), + ), + ), + ), + ), + ], + ); + } +} + +class OrderByActionPage extends StatefulWidget { + const OrderByActionPage({ + Key? key, + required this.items, + }) : super(key: key); + + final List items; + + @override + State createState() => _OrderByActionPageState(); +} + +class _OrderByActionPageState extends State { + final List _items = []; + + bool isEdit = false; + + @override + void initState() { + super.initState(); + _items.addAll(widget.items); + } + + Future sureBack() { + if (isEdit) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Are you sure?'), + content: const Text('You have not saved the changes.'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('Sure'), + ), + ], + ), + ).then((value) => value == true); + } else { + return Future.value(true); + } + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: sureBack, + child: _buildBody(context), + ); + } + + Widget _buildBody(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Order By'), + actions: [ + IconButton( + onPressed: _addItem, + icon: const Icon(Icons.add), + ), + ], + ), + body: ListView.builder( + itemBuilder: _buildItem, + itemCount: _items.length, + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.pop(context, _items); + }, + child: const Icon(Icons.check), + ), + ); + } + + Widget _buildItem(BuildContext context, int index) { + final item = _items[index]; + return ListTile( + title: Text(item.column), + subtitle: Text(item.isAsc ? 'ASC' : 'DESC'), + trailing: IconButton( + onPressed: () { + setState(() { + isEdit = true; + _items.removeAt(index); + }); + }, + icon: const Icon(Icons.delete), + ), + ); + } + + Future _addItem() async { + final result = await showDialog( + context: context, + builder: _buildDialog, + ); + if (result != null) { + setState(() { + isEdit = true; + _items.add(result); + }); + } + } + + Widget _buildDialog(BuildContext context) { + final List columns; + if (Platform.isAndroid) { + columns = AndroidMediaColumns.values(); + } else if (Platform.isMacOS || Platform.isIOS) { + columns = DarwinColumns.values(); + } else { + return const SizedBox.shrink(); + } + String column = columns.first; + bool isAsc = true; + return AlertDialog( + title: const Text('Add Order By'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DropdownButtonFormField( + items: columns + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e), + ), + ) + .toList(), + onChanged: (value) { + if (value != null) { + column = value; + } + }, + decoration: const InputDecoration( + labelText: 'Column', + ), + value: columns.first, + ), + DropdownButtonFormField( + items: const [ + DropdownMenuItem( + value: true, + child: Text('ASC'), + ), + DropdownMenuItem( + value: false, + child: Text('DESC'), + ), + ], + onChanged: (value) { + if (value != null) { + isAsc = value; + } + }, + decoration: const InputDecoration( + labelText: 'Order', + ), + value: true, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + final item = OrderByItem(column, isAsc); + Navigator.pop(context, item); + }, + child: const Text('OK'), + ), + ], + ); + } +} diff --git a/example/lib/page/custom_filter/path_list.dart b/example/lib/page/custom_filter/path_list.dart new file mode 100644 index 00000000..5fd2d85a --- /dev/null +++ b/example/lib/page/custom_filter/path_list.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:oktoast/oktoast.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:photo_manager_example/page/custom_filter/path_page.dart'; + +class FilterPathList extends StatelessWidget { + final CustomFilter filter; + + const FilterPathList({Key? key, required this.filter}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: PhotoManager.getAssetPathList( + filterOption: filter, + ), + builder: ( + BuildContext context, + AsyncSnapshot> snapshot, + ) { + if (snapshot.hasData) { + return PathList(list: snapshot.data!); + } + return const SizedBox(); + }, + ); + } +} + +class PathList extends StatelessWidget { + const PathList({ + Key? key, + required this.list, + }) : super(key: key); + + final List list; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemBuilder: (BuildContext context, int index) { + final AssetPathEntity path = list[index]; + return ListTile( + title: Text(path.name), + subtitle: Text(path.id), + trailing: FutureBuilder( + future: path.assetCountAsync, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return Text(snapshot.data.toString()); + } + return const SizedBox(); + }, + ), + onTap: () { + path.assetCountAsync.then((value) { + showToast( + 'Asset count: $value', + position: ToastPosition.bottom, + ); + }); + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => PathPage(path: path), + ), + ); + }, + ); + }, + itemCount: list.length, + ); + } +} diff --git a/example/lib/page/custom_filter/path_page.dart b/example/lib/page/custom_filter/path_page.dart new file mode 100644 index 00000000..094c3a7c --- /dev/null +++ b/example/lib/page/custom_filter/path_page.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:photo_manager_example/page/custom_filter/image_list.dart'; + +class PathPage extends StatefulWidget { + const PathPage({Key? key, required this.path}) : super(key: key); + final AssetPathEntity path; + + @override + State createState() => _PathPageState(); +} + +class _PathPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.path.name), + ), + body: GalleryWidget( + path: widget.path, + ), + ); + } +} + +class GalleryWidget extends StatefulWidget { + const GalleryWidget({Key? key, required this.path}) : super(key: key); + + final AssetPathEntity path; + + @override + State createState() => _GalleryWidgetState(); +} + +class _GalleryWidgetState extends State { + List _list = []; + + @override + void initState() { + super.initState(); + _refresh(); + } + + Future _refresh() async { + final count = await widget.path.assetCountAsync; + if (count == 0) { + return; + } + final list = await widget.path.getAssetListRange(start: 0, end: count); + setState(() { + if (mounted) _list = list; + }); + } + + @override + Widget build(BuildContext context) { + return ImageList(list: _list); + } +} diff --git a/example/lib/page/custom_filter_example_page.dart b/example/lib/page/custom_filter_example_page.dart new file mode 100644 index 00000000..3d07a3af --- /dev/null +++ b/example/lib/page/custom_filter_example_page.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:photo_manager_example/page/custom_filter/path_list.dart'; + +import 'custom_filter/advance_filter_page.dart'; +import 'custom_filter/custom_filter_sql_page.dart'; +import 'custom_filter/filter_assets_page.dart'; + +class CustomFilterExamplePage extends StatelessWidget { + const CustomFilterExamplePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget buildItem(String title, Widget target) { + return ListTile( + title: Text(title), + onTap: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => target)); + }, + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text('Custom Filter Example'), + ), + body: Center( + child: Column( + children: [ + buildItem( + 'Custom Filter with sql', + CustomFilterSqlPage( + builder: (BuildContext context, CustomFilter filter) { + return FilterPathList(filter: filter); + }, + ), + ), + buildItem( + 'Advanced Custom Filter', + AdvancedCustomFilterPage( + builder: (BuildContext context, CustomFilter filter) { + return FilterPathList(filter: filter); + }, + ), + ), + buildItem( + 'Custom Filter sql assets', + CustomFilterSqlPage( + builder: (BuildContext context, CustomFilter filter) { + return FilterAssetsContent(filter: filter); + }, + ), + ), + buildItem( + 'Advanced Custom Filter assets', + AdvancedCustomFilterPage( + builder: (BuildContext context, CustomFilter filter) { + return FilterAssetsContent(filter: filter); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/page/developer/android/column_names_page.dart b/example/lib/page/developer/android/column_names_page.dart new file mode 100644 index 00000000..a6e5b643 --- /dev/null +++ b/example/lib/page/developer/android/column_names_page.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:photo_manager/photo_manager.dart'; + +class ColumnNamesPage extends StatefulWidget { + const ColumnNamesPage({Key? key}) : super(key: key); + + @override + State createState() => _ColumnNamesPageState(); +} + +class _ColumnNamesPageState extends State { + List _columns = []; + + void _refresh() async { + final columns = await PhotoManager.plugin.androidColumns(); + print('columns: $columns'); + columns.sort(); + setState(() { + _columns = columns; + }); + } + + @override + void initState() { + _refresh(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Column Names'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _refresh, + ), + ], + ), + body: _buildBody(), + ); + } + + Widget _buildBody() { + final list = _columns; + return ListView.builder( + itemBuilder: (context, index) { + final column = list[index]; + return ListTile( + title: Text(column), + subtitle: const Text('click to copy'), + onTap: () { + Clipboard.setData(ClipboardData(text: column)); + }, + ); + }, + itemCount: list.length, + ); + } +} diff --git a/example/lib/page/developer/custom_filter_page.dart b/example/lib/page/developer/custom_filter_page.dart new file mode 100644 index 00000000..ba242d28 --- /dev/null +++ b/example/lib/page/developer/custom_filter_page.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:oktoast/oktoast.dart'; +import 'package:photo_manager/photo_manager.dart'; + +class CustomFilterPage extends StatefulWidget { + const CustomFilterPage({Key? key}) : super(key: key); + + @override + State createState() => _CustomFilterPageState(); +} + +class _CustomFilterPageState extends State { + static const columns = CustomColumns.base; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('DevCustomFilterPage'), + ), + body: Center( + child: Column(children: [ + const Text( + 'Click button to show filter log in console', + textAlign: TextAlign.center, + ), + ...[ + filterButton(_sqlFilter), + filterButton(_widthFilter), + filterButton(_advancedFilter), + ].map( + (e) => Container( + // alignment: Alignment.center, + width: double.infinity, + margin: const EdgeInsets.all(8.0), + child: e, + ), + ) + ]), + ), + ); + } + + Widget filterButton(ValueGetter filterBuilder) { + final filter = filterBuilder(); + return ListTile( + title: Text('Filter: ${filter.makeWhere()}'), + subtitle: Text('Order by: ${filter.makeOrderBy()}'), + onTap: () async { + final where = filter.makeWhere(); + final orderBy = filter.makeOrderBy(); + + print('where: $where'); + print('orderBy: $orderBy'); + + final permissionResult = await PhotoManager.requestPermissionExtend(); + if (!permissionResult.hasAccess) { + showToast('No permission to access photo'); + return; + } + + const type = RequestType.all; + + final assetCount = await PhotoManager.getAssetCount( + type: type, + filterOption: filter, + ); + + print('The asset count is $assetCount'); + + final assetList = await PhotoManager.getAssetListPaged( + page: 0, + pageCount: 20, + filterOption: filter, + type: type, + ); + + for (final asset in assetList) { + final info = StringBuffer(); + info.writeln('id: ${asset.id}'); + info.writeln(' type: ${asset.type}'); + info.writeln(' width: ${asset.width}'); + info.writeln(' height: ${asset.height}'); + info.writeln(' duration: ${asset.duration}'); + info.writeln(' size: ${asset.size}'); + info.writeln(' createDt: ${asset.createDateTime}'); + info.writeln(' modifiedDt: ${asset.modifiedDateTime}'); + info.writeln(' latitude: ${asset.latitude}'); + info.writeln(' longitude: ${asset.longitude}'); + info.writeln(' orientation: ${asset.orientation}'); + info.writeln(' isFavorite: ${asset.isFavorite}'); + + info.writeln(); + + print(info); + } + }, + ); + } + + CustomFilter _sqlFilter() { + return CustomFilter.sql( + where: '', + orderBy: [ + OrderByItem.desc(CustomColumns.base.width), + ], + ); + } + + CustomFilter _widthFilter() { + return CustomFilter.sql( + where: '${columns.width} >= 1000', + orderBy: [ + OrderByItem.desc(CustomColumns.base.width), + ], + ); + } + + CustomFilter _advancedFilter() { + final subGroup1 = WhereConditionGroup() + .and( + ColumnWhereCondition( + column: columns.height, operator: '<', value: '100'), + ) + .or( + ColumnWhereCondition( + column: columns.height, operator: '>', value: '1000'), + ); + + final subGroup2 = WhereConditionGroup() + .and( + ColumnWhereCondition( + column: columns.width, operator: '<', value: '200'), + ) + .or( + ColumnWhereCondition( + column: columns.width, operator: '>', value: '1000'), + ); + + final dateColumn = columns.createDate; + final date = DateTime.now().subtract(const Duration(days: 30)); + + final dateItem = DateColumnWhereCondition( + column: dateColumn, operator: '>', value: date); + + final whereGroup = WhereConditionGroup() + .and( + subGroup1, + ) + .and( + subGroup2, + ) + .and(dateItem); + + final filter = AdvancedCustomFilter() + .addOrderBy( + column: columns.createDate, + isAsc: false, + ) + .addWhereCondition(whereGroup); + + return filter; + } +} diff --git a/example/lib/page/developer/develop_index_page.dart b/example/lib/page/developer/develop_index_page.dart index 314b24c8..a412005c 100644 --- a/example/lib/page/developer/develop_index_page.dart +++ b/example/lib/page/developer/develop_index_page.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:photo_manager/photo_manager.dart'; +import 'package:photo_manager_example/page/developer/android/column_names_page.dart'; +import 'package:photo_manager_example/page/developer/custom_filter_page.dart'; import '../../util/log.dart'; import 'create_entity_by_id.dart'; @@ -37,6 +39,15 @@ class _DeveloperIndexPageState extends State { body: ListView( padding: const EdgeInsets.all(8.0), children: [ + ElevatedButton( + onPressed: () => navToWidget(const CustomFilterPage()), + child: const Text('Custom filter'), + ), + if (Platform.isAndroid) + ElevatedButton( + onPressed: () => navToWidget(const ColumnNamesPage()), + child: const Text('Android: column names'), + ), ElevatedButton( child: const Text('Show iOS create folder example.'), onPressed: () => navToWidget(const CreateFolderExample()), @@ -91,7 +102,13 @@ class _DeveloperIndexPageState extends State { onPressed: _persentLimited, child: const Text('PresentLimited'), ), - ], + ] + .map((e) => Container( + padding: const EdgeInsets.all(3.0), + height: 44, + child: e, + )) + .toList(), ), ); } diff --git a/example/lib/page/home_page.dart b/example/lib/page/home_page.dart index 332895bb..c769857b 100644 --- a/example/lib/page/home_page.dart +++ b/example/lib/page/home_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:photo_manager/photo_manager.dart'; +import 'package:photo_manager_example/widget/nav_button.dart'; import 'package:provider/provider.dart'; import '../model/photo_provider.dart'; @@ -40,11 +41,14 @@ class _NewHomePageState extends State { body: ListView( padding: const EdgeInsets.all(8.0), children: [ - buildButton('Get all gallery list', _scanGalleryList), + CustomButton( + title: 'Get all gallery list', + onPressed: _scanGalleryList, + ), if (Platform.isIOS) - buildButton( - 'Change limited photos with PhotosUI', - _changeLimitPhotos, + CustomButton( + title: 'Change limited photos with PhotosUI', + onPressed: _changeLimitPhotos, ), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -213,8 +217,8 @@ class _NewHomePageState extends State { void onChange(MethodCall call) {} Widget _buildFilterOption(PhotoProvider provider) { - return ElevatedButton( - child: const Text('Change filter options.'), + return CustomButton( + title: 'Change filter options.', onPressed: () { Navigator.push( context, diff --git a/example/lib/page/image_list_page.dart b/example/lib/page/image_list_page.dart index a361dd7c..4c662448 100644 --- a/example/lib/page/image_list_page.dart +++ b/example/lib/page/image_list_page.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; diff --git a/example/lib/page/index_page.dart b/example/lib/page/index_page.dart index bee8a857..6f9b5eeb 100644 --- a/example/lib/page/index_page.dart +++ b/example/lib/page/index_page.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:photo_manager_example/page/custom_filter_example_page.dart'; +import 'package:photo_manager_example/widget/nav_button.dart'; import 'change_notify_page.dart'; import 'developer/develop_index_page.dart'; @@ -22,22 +24,17 @@ class _IndexPageState extends State { body: ListView( padding: const EdgeInsets.all(8.0), children: [ - routePage('gallery list', const NewHomePage()), - routePage('save media example', const SaveMediaExample()), - routePage('For Developer page', const DeveloperIndexPage()), + routePage('Gallery list', const NewHomePage()), + routePage('Custom filter example', const CustomFilterExamplePage()), + routePage('Save media example', const SaveMediaExample()), routePage('Change notify example', const ChangeNotifyExample()), + routePage('For Developer page', const DeveloperIndexPage()), ], ), ); } Widget routePage(String title, Widget page) { - return ElevatedButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => page), - ), - child: Text(title), - ); + return NavButton(title: title, page: page); } } diff --git a/example/lib/util/common_util.dart b/example/lib/util/common_util.dart index 8defeea4..29c48a70 100644 --- a/example/lib/util/common_util.dart +++ b/example/lib/util/common_util.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:oktoast/oktoast.dart'; diff --git a/example/lib/widget/nav_button.dart b/example/lib/widget/nav_button.dart new file mode 100644 index 00000000..75350bc1 --- /dev/null +++ b/example/lib/widget/nav_button.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class NavButton extends StatelessWidget { + const NavButton({ + Key? key, + required this.title, + required this.page, + }) : super(key: key); + + final String title; + final Widget page; + + @override + Widget build(BuildContext context) { + return CustomButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => page), + ), + title: title, + ); + } +} + +class CustomButton extends StatelessWidget { + const CustomButton({ + Key? key, + required this.title, + required this.onPressed, + }) : super(key: key); + + final String title; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8.0), + height: 64, + child: ElevatedButton( + onPressed: onPressed, + child: Text(title), + ), + ); + } +} diff --git a/example/macos/.gitignore b/example/macos/.gitignore index d2fd3772..daee9623 100644 --- a/example/macos/.gitignore +++ b/example/macos/.gitignore @@ -4,3 +4,5 @@ # Xcode-related **/xcuserdata/ + +Flutter/GeneratedPluginRegistrant.swift \ No newline at end of file diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index c1ab184f..00000000 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import path_provider_macos -import photo_manager - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin")) -} diff --git a/example/macos/Podfile b/example/macos/Podfile index f976ae51..9ec46f8c 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -26,38 +26,11 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_macos_podfile_setup -def install_plugin_pods(application_path = nil, relative_symlink_dir, platform) - # defined_in_file is set by CocoaPods and is a Pathname to the Podfile. - application_path ||= File.dirname(defined_in_file.realpath) if self.respond_to?(:defined_in_file) - raise 'Could not find application path' unless application_path - - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - - symlink_dir = File.expand_path(relative_symlink_dir, application_path) - system('rm', '-rf', symlink_dir) # Avoid the complication of dependencies like FileUtils. - - symlink_plugins_dir = File.expand_path('plugins', symlink_dir) - system('mkdir', '-p', symlink_plugins_dir) - - plugins_file = File.join(application_path, '..', '.flutter-plugins-dependencies') - plugin_pods = flutter_parse_plugins_file(plugins_file, platform) - plugin_pods.each do |plugin_hash| - plugin_name = plugin_hash['name'] - plugin_path = plugin_hash['path'] - if (plugin_name && plugin_path) - specPath = "#{plugin_path}/#{platform}/#{plugin_name}.podspec" - pod plugin_name, :path => specPath - end - end -end - target 'Runner' do use_frameworks! use_modular_headers! - flutter_install_macos_engine_pod(File.dirname(File.realpath(__FILE__))) - install_plugin_pods(File.dirname(File.realpath(__FILE__)), '.symlinks', 'macos') + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 93a93ba8..95c6690d 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -57,7 +57,7 @@ 243563986A8A6E8752F5FC9C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* photo_manager Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "photo_manager Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -112,7 +112,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* example.app */, + 33CC10ED2044A3C60003C045 /* photo_manager Example.app */, ); name = Products; sourceTree = ""; @@ -192,7 +192,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productReference = 33CC10ED2044A3C60003C045 /* photo_manager Example.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -419,9 +419,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = S5GU4EMC47; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -548,9 +549,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = S5GU4EMC47; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -571,9 +573,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = S5GU4EMC47; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 855d856d..d60f3a55 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index 8e2664ca..4ce65413 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -6,6 +6,8 @@ com.apple.security.assets.movies.read-write + com.apple.security.assets.music.read-write + com.apple.security.assets.pictures.read-write com.apple.security.cs.allow-jit diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c40a7d7e..571bd131 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -21,6 +21,9 @@ dependencies: dev_dependencies: flutter_lints: any + flutter_test: + sdk: flutter + test: any flutter: uses-material-design: true diff --git a/flow_chart/advance_custom_filter.dio b/flow_chart/advance_custom_filter.dio new file mode 100644 index 00000000..ee6bd580 --- /dev/null +++ b/flow_chart/advance_custom_filter.dio @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flow_chart/advance_custom_filter.png b/flow_chart/advance_custom_filter.png new file mode 100644 index 00000000..5c5a9b75 Binary files /dev/null and b/flow_chart/advance_custom_filter.png differ diff --git a/ios/Classes/PMNotificationManager.m b/ios/Classes/PMNotificationManager.m index 46615309..4d8609cf 100644 --- a/ios/Classes/PMNotificationManager.m +++ b/ios/Classes/PMNotificationManager.m @@ -111,11 +111,13 @@ - (void)addToResult:(NSMutableDictionary *)dictionary } - (PHFetchResult *)getLastAssets { +#if __IPHONE_14_0 if (@available(iOS 14, *)) { if (PHPhotoLibrary.authorizationStatus == PHAuthorizationStatusLimited) { return [PHAsset fetchAssetsWithOptions:nil]; } } +#endif if (PHPhotoLibrary.authorizationStatus == PHAuthorizationStatusAuthorized) { return [PHAsset fetchAssetsWithOptions:nil]; } diff --git a/ios/Classes/PMPlugin.h b/ios/Classes/PMPlugin.h index 2eba75c3..246c538f 100644 --- a/ios/Classes/PMPlugin.h +++ b/ios/Classes/PMPlugin.h @@ -1,5 +1,6 @@ #import #import "PMImport.h" +#import @class PMManager; @class PMNotificationManager; diff --git a/ios/Classes/PMPlugin.m b/ios/Classes/PMPlugin.m index 22ed4e14..007c8ab3 100644 --- a/ios/Classes/PMPlugin.m +++ b/ios/Classes/PMPlugin.m @@ -1,7 +1,6 @@ #import "PMPlugin.h" #import "PMConvertUtils.h" #import "PMAssetPathEntity.h" -#import "PMFilterOption.h" #import "PMLogUtils.h" #import "PMManager.h" #import "PMNotificationManager.h" @@ -10,7 +9,6 @@ #import "PMProgressHandler.h" #import "PMConverter.h" -#import #import @implementation PMPlugin { @@ -21,17 +19,17 @@ @implementation PMPlugin { - (void)registerPlugin:(NSObject *)registrar { privateRegistrar = registrar; [self initNotificationManager:registrar]; - + FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"com.fluttercandies/photo_manager" - binaryMessenger:[registrar messenger]]; + [FlutterMethodChannel methodChannelWithName:@"com.fluttercandies/photo_manager" + binaryMessenger:[registrar messenger]]; PMManager *manager = [PMManager new]; manager.converter = [PMConverter new]; [self setManager:manager]; [channel - setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { - [self onMethodCall:call result:result]; - }]; + setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { + [self onMethodCall:call result:result]; + }]; } - (void)initNotificationManager:(NSObject *)registrar { @@ -41,7 +39,7 @@ - (void)initNotificationManager:(NSObject *)registrar { - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { ResultHandler *handler = [ResultHandler handlerWithResult:result]; PMManager *manager = self.manager; - + if ([call.method isEqualToString:@"requestPermissionExtend"]) { int requestAccessLevel = [call.arguments[@"iosAccessLevel"] intValue]; [self handlePermission:manager handler:handler requestAccessLevel:requestAccessLevel]; @@ -51,7 +49,7 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { [manager clearFileCache]; [handler reply:@1]; } else if ([call.method isEqualToString:@"openSetting"]) { - [PMManager openSetting: handler]; + [PMManager openSetting:handler]; } else if ([call.method isEqualToString:@"ignorePermissionCheck"]) { ignoreCheckPermission = [call.arguments[@"ignore"] boolValue]; [handler reply:@(ignoreCheckPermission)]; @@ -65,27 +63,28 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { [self onAuth:call result:result]; } else { [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - BOOL auth = PHAuthorizationStatusAuthorized == status; - [manager setAuth:auth]; - if (auth) { - [self onAuth:call result:result]; - } else { - [handler replyError:@"need permission"]; - } + BOOL auth = PHAuthorizationStatusAuthorized == status; + [manager setAuth:auth]; + if (auth) { + [self onAuth:call result:result]; + } else { + [handler replyError:@"need permission"]; + } }]; } } } --(void)replyPermssionResult:(ResultHandler*) handler status:(PHAuthorizationStatus)status{ +- (void)replyPermssionResult:(ResultHandler *)handler status:(PHAuthorizationStatus)status { BOOL auth = PHAuthorizationStatusAuthorized == status; [self.manager setAuth:auth]; [handler reply:@(status)]; } #if TARGET_OS_IOS -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 -- (UIViewController*)getCurrentViewController { +#if __IPHONE_14_0 + +- (UIViewController *)getCurrentViewController { UIViewController *controller = UIApplication.sharedApplication.keyWindow.rootViewController; if (controller) { UIViewController *result = controller; @@ -99,19 +98,20 @@ - (UIViewController*)getCurrentViewController { } return nil; } + #endif - (void)handlePermission:(PMManager *)manager - handler:(ResultHandler*)handler + handler:(ResultHandler *)handler requestAccessLevel:(int)requestAccessLevel { -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 +#if __IPHONE_14_0 if (@available(iOS 14, *)) { [PHPhotoLibrary requestAuthorizationForAccessLevel:requestAccessLevel handler:^(PHAuthorizationStatus status) { - [self replyPermssionResult:handler status:status]; + [self replyPermssionResult:handler status:status]; }]; } else { [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - [self replyPermssionResult:handler status:status]; + [self replyPermssionResult:handler status:status]; }]; } #else @@ -123,14 +123,14 @@ - (void)handlePermission:(PMManager *)manager - (void)requestPermissionStatus:(int)requestAccessLevel completeHandler:(void (^)(PHAuthorizationStatus status))completeHandler { -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 +#if __IPHONE_14_0 if (@available(iOS 14, *)) { [PHPhotoLibrary requestAuthorizationForAccessLevel:requestAccessLevel handler:^(PHAuthorizationStatus status) { - completeHandler(status); + completeHandler(status); }]; } else { [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - completeHandler(status); + completeHandler(status); }]; } #else @@ -140,26 +140,26 @@ - (void)requestPermissionStatus:(int)requestAccessLevel #endif } -- (void)presentLimited:(ResultHandler*)handler { +- (void)presentLimited:(ResultHandler *)handler { #if __IPHONE_14_0 if (@available(iOS 14, *)) { - UIViewController* controller = [self getCurrentViewController]; + UIViewController *controller = [self getCurrentViewController]; if (!controller) { [handler reply:[FlutterError - errorWithCode:@"UIViewController is nil" - message:@"presentLimited require a valid UIViewController." - details:nil]]; + errorWithCode:@"UIViewController is nil" + message:@"presentLimited require a valid UIViewController." + details:nil]]; return; } #if __IPHONE_15_0 if (@available(iOS 15, *)) { [PHPhotoLibrary.sharedPhotoLibrary - presentLimitedLibraryPickerFromViewController: controller - completionHandler:^(NSArray * _Nonnull list) { - [handler reply: list]; - }]; + presentLimitedLibraryPickerFromViewController:controller + completionHandler:^(NSArray *_Nonnull list) { + [handler reply:list]; + }]; } else { - [PHPhotoLibrary.sharedPhotoLibrary presentLimitedLibraryPickerFromViewController: controller]; + [PHPhotoLibrary.sharedPhotoLibrary presentLimitedLibraryPickerFromViewController:controller]; [handler reply:nil]; } #else @@ -172,6 +172,7 @@ - (void)presentLimited:(ResultHandler*)handler { [handler reply:nil]; #endif } + #endif #if TARGET_OS_OSX @@ -228,288 +229,311 @@ - (void)onAuth:(FlutterMethodCall *)call result:(FlutterResult)result { ResultHandler *handler = [ResultHandler handlerWithResult:result]; PMManager *manager = self.manager; __block PMNotificationManager *notificationManager = self.notificationManager; - + [self runInBackground:^{ - if ([call.method isEqualToString:@"getAssetPathList"]) { - int type = [call.arguments[@"type"] intValue]; - BOOL hasAll = [call.arguments[@"hasAll"] boolValue]; - BOOL onlyAll = [call.arguments[@"onlyAll"] boolValue]; - PMFilterOptionGroup *option = + @try { + [self handleMethodResult:call handler:handler manager:manager notificationManager:notificationManager]; + } + @catch (NSException *exception) { + [handler replyError:exception.reason]; + } + }]; +} + +- (void)handleMethodResult:(FlutterMethodCall *)call handler:(ResultHandler *)handler manager:(PMManager *)manager notificationManager:(PMNotificationManager *)notificationManager { + if ([call.method isEqualToString:@"getAssetPathList"]) { + int type = [call.arguments[@"type"] intValue]; + BOOL hasAll = [call.arguments[@"hasAll"] boolValue]; + BOOL onlyAll = [call.arguments[@"onlyAll"] boolValue]; + NSObject *option = [PMConvertUtils convertMapToOptionContainer:call.arguments[@"option"]]; - NSArray *array = [manager - getAssetPathList:type - hasAll:hasAll - onlyAll:onlyAll - option:option]; - NSDictionary *dictionary = [PMConvertUtils convertPathToMap:array]; - [handler reply:dictionary]; - } else if ([call.method isEqualToString:@"getAssetCountFromPath"]) { - NSString *id = call.arguments[@"id"]; - int requestType = [call.arguments[@"type"] intValue]; - PMFilterOptionGroup *option = + NSArray *array = [manager getAssetPathList:type + hasAll:hasAll + onlyAll:onlyAll + option:option]; + NSDictionary *dictionary = [PMConvertUtils convertPathToMap:array]; + [handler reply:dictionary]; + } else if ([call.method isEqualToString:@"getAssetCountFromPath"]) { + NSString *id = call.arguments[@"id"]; + int requestType = [call.arguments[@"type"] intValue]; + NSObject *option = [PMConvertUtils convertMapToOptionContainer:call.arguments[@"option"]]; - NSUInteger result = [manager getAssetCountFromPath:id + NSUInteger assetCount = [manager getAssetCountFromPath:id type:requestType filterOption:option]; - [handler reply:@(result)]; - } else if ([call.method isEqualToString:@"getAssetListPaged"]) { - NSString *id = call.arguments[@"id"]; - int type = [call.arguments[@"type"] intValue]; - NSUInteger page = [call.arguments[@"page"] unsignedIntValue]; - NSUInteger size = [call.arguments[@"size"] unsignedIntValue]; - PMFilterOptionGroup *option = + [handler reply:@(assetCount)]; + } else if ([call.method isEqualToString:@"getAssetListPaged"]) { + NSString *id = call.arguments[@"id"]; + int type = [call.arguments[@"type"] intValue]; + NSUInteger page = [call.arguments[@"page"] unsignedIntValue]; + NSUInteger size = [call.arguments[@"size"] unsignedIntValue]; + NSObject *option = [PMConvertUtils convertMapToOptionContainer:call.arguments[@"option"]]; - NSArray *array = + NSArray *array = [manager getAssetListPaged:id type:type page:page size:size filterOption:option]; - NSDictionary *dictionary = + NSDictionary *dictionary = [PMConvertUtils convertAssetToMap:array optionGroup:option]; - [handler reply:dictionary]; - } else if ([call.method isEqualToString:@"getAssetListRange"]) { - NSString *id = call.arguments[@"id"]; - int type = [call.arguments[@"type"] intValue]; - NSUInteger start = [call.arguments[@"start"] unsignedIntegerValue]; - NSUInteger end = [call.arguments[@"end"] unsignedIntegerValue]; - PMFilterOptionGroup *option = + [handler reply:dictionary]; + } else if ([call.method isEqualToString:@"getAssetListRange"]) { + NSString *id = call.arguments[@"id"]; + int type = [call.arguments[@"type"] intValue]; + NSUInteger start = [call.arguments[@"start"] unsignedIntegerValue]; + NSUInteger end = [call.arguments[@"end"] unsignedIntegerValue]; + NSObject *option = [PMConvertUtils convertMapToOptionContainer:call.arguments[@"option"]]; - NSArray *array = + NSArray *array = [manager getAssetListRange:id type:type start:start end:end filterOption:option]; - NSDictionary *dictionary = + NSDictionary *dictionary = [PMConvertUtils convertAssetToMap:array optionGroup:option]; - [handler reply:dictionary]; - } else if ([call.method isEqualToString:@"getThumb"]) { - NSString *id = call.arguments[@"id"]; - NSDictionary *dict = call.arguments[@"option"]; - PMProgressHandler *progressHandler = [self getProgressHandlerFromDict:call.arguments]; - PMThumbLoadOption *option = [PMThumbLoadOption optionDict:dict]; - [manager getThumbWithId:id - option:option - resultHandler:handler - progressHandler:progressHandler]; - } else if ([call.method isEqualToString:@"getFullFile"]) { - NSString *id = call.arguments[@"id"]; - BOOL isOrigin = [call.arguments[@"isOrigin"] boolValue]; - int subtype = [call.arguments[@"subtype"] intValue]; - PMProgressHandler *progressHandler = [self getProgressHandlerFromDict:call.arguments]; - [manager getFullSizeFileWithId:id - isOrigin:isOrigin - subtype:subtype - resultHandler:handler - progressHandler:progressHandler]; - } else if ([call.method isEqualToString:@"releaseMemoryCache"]) { - [manager clearCache]; - [handler reply:nil]; - } else if ([call.method isEqualToString:@"fetchPathProperties"]) { - NSString *id = call.arguments[@"id"]; - int requestType = [call.arguments[@"type"] intValue]; - PMFilterOptionGroup *option = + [handler reply:dictionary]; + } else if ([call.method isEqualToString:@"getThumb"]) { + NSString *id = call.arguments[@"id"]; + NSDictionary *dict = call.arguments[@"option"]; + PMProgressHandler *progressHandler = [self getProgressHandlerFromDict:call.arguments]; + PMThumbLoadOption *option = [PMThumbLoadOption optionDict:dict]; + [manager getThumbWithId:id + option:option + resultHandler:handler + progressHandler:progressHandler]; + } else if ([call.method isEqualToString:@"getFullFile"]) { + NSString *id = call.arguments[@"id"]; + BOOL isOrigin = [call.arguments[@"isOrigin"] boolValue]; + int subtype = [call.arguments[@"subtype"] intValue]; + PMProgressHandler *progressHandler = [self getProgressHandlerFromDict:call.arguments]; + [manager getFullSizeFileWithId:id + isOrigin:isOrigin + subtype:subtype + resultHandler:handler + progressHandler:progressHandler]; + } else if ([call.method isEqualToString:@"releaseMemoryCache"]) { + [manager clearCache]; + [handler reply:nil]; + } else if ([call.method isEqualToString:@"fetchPathProperties"]) { + NSString *id = call.arguments[@"id"]; + int requestType = [call.arguments[@"type"] intValue]; + NSObject *option = [PMConvertUtils convertMapToOptionContainer:call.arguments[@"option"]]; - PMAssetPathEntity *pathEntity = [manager fetchPathProperties:id - type:requestType - filterOption:option]; - if (option.containsModified) { - [manager injectModifyToDate:pathEntity]; - } - if (pathEntity) { - NSDictionary *dictionary = + PMAssetPathEntity *pathEntity = [manager fetchPathProperties:id + type:requestType + filterOption:option]; + if (option.containsModified) { + [manager injectModifyToDate:pathEntity]; + } + if (pathEntity) { + NSDictionary *dictionary = [PMConvertUtils convertPathToMap:@[pathEntity]]; - [handler reply:dictionary]; - } else { - [handler reply:nil]; - } - } else if ([call.method isEqualToString:@"notify"]) { - BOOL notify = [call.arguments[@"notify"] boolValue]; - if (notify) { - [notificationManager startNotify]; - } else { - [notificationManager stopNotify]; - } + [handler reply:dictionary]; + } else { [handler reply:nil]; - } else if ([call.method isEqualToString:@"isNotifying"]) { - BOOL isNotifying = [notificationManager isNotifying]; - [handler reply:@(isNotifying)]; - - } else if ([call.method isEqualToString:@"deleteWithIds"]) { - NSArray *ids = call.arguments[@"ids"]; - [manager deleteWithIds:ids - changedBlock:^(NSArray *array) { - [handler reply:array]; - }]; - } else if ([call.method isEqualToString:@"saveImage"]) { - NSData *data = [call.arguments[@"image"] data]; - NSString *title = call.arguments[@"title"]; - NSString *desc = call.arguments[@"desc"]; - [manager saveImage:data - title:title - desc:desc - block:^(PMAssetEntity *asset) { - if (!asset) { - [handler reply:nil]; - return; - } - [handler reply:[PMConvertUtils convertPMAssetToMap:asset needTitle:NO]]; - }]; - } else if ([call.method isEqualToString:@"saveImageWithPath"]) { - NSString *path = call.arguments[@"path"]; - NSString *title = call.arguments[@"title"]; - NSString *desc = call.arguments[@"desc"]; - [manager saveImageWithPath:path - title:title - desc:desc - block:^(PMAssetEntity *asset) { - if (!asset) { - [handler reply:nil]; - return; - } - [handler reply:[PMConvertUtils convertPMAssetToMap:asset needTitle:NO]]; - }]; - } else if ([call.method isEqualToString:@"saveVideo"]) { - NSString *videoPath = call.arguments[@"path"]; - NSString *title = call.arguments[@"title"]; - NSString *desc = call.arguments[@"desc"]; - [manager saveVideo:videoPath + } + } else if ([call.method isEqualToString:@"notify"]) { + BOOL notify = [call.arguments[@"notify"] boolValue]; + if (notify) { + [notificationManager startNotify]; + } else { + [notificationManager stopNotify]; + } + [handler reply:nil]; + } else if ([call.method isEqualToString:@"isNotifying"]) { + BOOL isNotifying = [notificationManager isNotifying]; + [handler reply:@(isNotifying)]; + + } else if ([call.method isEqualToString:@"deleteWithIds"]) { + NSArray *ids = call.arguments[@"ids"]; + [manager deleteWithIds:ids + changedBlock:^(NSArray *array) { + [handler reply:array]; + }]; + } else if ([call.method isEqualToString:@"saveImage"]) { + NSData *data = [call.arguments[@"image"] data]; + NSString *title = call.arguments[@"title"]; + NSString *desc = call.arguments[@"desc"]; + [manager saveImage:data + title:title + desc:desc + block:^(PMAssetEntity *asset) { + if (!asset) { + [handler reply:nil]; + return; + } + [handler reply:[PMConvertUtils convertPMAssetToMap:asset needTitle:NO]]; + }]; + } else if ([call.method isEqualToString:@"saveImageWithPath"]) { + NSString *path = call.arguments[@"path"]; + NSString *title = call.arguments[@"title"]; + NSString *desc = call.arguments[@"desc"]; + [manager saveImageWithPath:path + title:title + desc:desc + block:^(PMAssetEntity *asset) { + if (!asset) { + [handler reply:nil]; + return; + } + [handler reply:[PMConvertUtils convertPMAssetToMap:asset needTitle:NO]]; + }]; + } else if ([call.method isEqualToString:@"saveVideo"]) { + NSString *videoPath = call.arguments[@"path"]; + NSString *title = call.arguments[@"title"]; + NSString *desc = call.arguments[@"desc"]; + [manager saveVideo:videoPath + title:title + desc:desc + block:^(PMAssetEntity *asset) { + if (!asset) { + [handler reply:nil]; + return; + } + [handler reply:[PMConvertUtils convertPMAssetToMap:asset needTitle:NO]]; + }]; + } else if ([call.method isEqualToString:@"saveLivePhoto"]) { + NSString *videoPath = call.arguments[@"videoPath"]; + NSString *imagePath = call.arguments[@"imagePath"]; + NSString *title = call.arguments[@"title"]; + NSString *desc = call.arguments[@"desc"]; + [manager saveLivePhoto:imagePath + videoPath:videoPath title:title desc:desc block:^(PMAssetEntity *asset) { - if (!asset) { - [handler reply:nil]; - return; - } - [handler reply:[PMConvertUtils convertPMAssetToMap:asset needTitle:NO]]; - }]; - } else if ([call.method isEqualToString:@"saveLivePhoto"]) { - NSString *videoPath = call.arguments[@"videoPath"]; - NSString *imagePath = call.arguments[@"imagePath"]; - NSString *title = call.arguments[@"title"]; - NSString *desc = call.arguments[@"desc"]; - [manager saveLivePhoto:imagePath - videoPath:videoPath - title:title - desc:desc - block:^(PMAssetEntity *asset) { - if (!asset) { - [handler reply:nil]; - return; - } - [handler reply:[PMConvertUtils convertPMAssetToMap:asset needTitle:NO]]; - }]; - } else if ([call.method isEqualToString:@"assetExists"]) { - NSString *assetId = call.arguments[@"id"]; - BOOL exists = [manager existsWithId:assetId]; - [handler reply:@(exists)]; - } else if ([call.method isEqualToString:@"isLocallyAvailable"]) { - NSString *assetId = call.arguments[@"id"]; - BOOL isOrigin = [call.arguments[@"isOrigin"] boolValue]; - BOOL exists = [manager entityIsLocallyAvailable:assetId resource:nil isOrigin:isOrigin]; - [handler reply:@(exists)]; - } else if ([call.method isEqualToString:@"getTitleAsync"]) { - NSString *assetId = call.arguments[@"id"]; - int subtype = [call.arguments[@"subtype"] intValue]; - NSString *title = [manager getTitleAsyncWithAssetId:assetId subtype:subtype]; - [handler reply:title]; - } else if ([call.method isEqualToString:@"getMimeTypeAsync"]) { - NSString *assetId = call.arguments[@"id"]; - NSString *mimeType = [manager getMimeTypeAsyncWithAssetId:assetId]; - [handler reply:mimeType]; - } else if ([@"getMediaUrl" isEqualToString:call.method]) { - [manager getMediaUrl:call.arguments[@"id"] resultHandler:handler]; - } else if ([@"fetchEntityProperties" isEqualToString:call.method]) { - NSString *assetId = call.arguments[@"id"]; - PMAssetEntity *entity = [manager getAssetEntity:assetId withCache:NO]; - if (entity == nil) { - [handler reply:nil]; - return; - } - [handler reply:[PMConvertUtils convertPMAssetToMap:entity needTitle:YES]]; - } else if ([@"getSubPath" isEqualToString:call.method]) { - NSString *galleryId = call.arguments[@"id"]; - int type = [call.arguments[@"type"] intValue]; - int albumType = [call.arguments[@"albumType"] intValue]; - NSDictionary *optionMap = call.arguments[@"option"]; - PMFilterOptionGroup *option = [PMConvertUtils convertMapToOptionContainer:optionMap]; - - NSArray *array = [manager getSubPathWithId:galleryId type:type albumType:albumType option:option]; - NSDictionary *pathData = [PMConvertUtils convertPathToMap:array]; - - [handler reply:@{@"list": pathData}]; - } else if ([@"copyAsset" isEqualToString:call.method]) { - NSString *assetId = call.arguments[@"assetId"]; - NSString *galleryId = call.arguments[@"galleryId"]; - [manager copyAssetWithId:assetId toGallery:galleryId block:^(PMAssetEntity *entity, NSString *msg) { - if (msg) { - NSLog(@"copy asset error, cause by : %@", msg); - [handler reply:nil]; - } else { - [handler reply:[PMConvertUtils convertPMAssetToMap:entity needTitle:NO]]; - } - }]; - } else if ([@"createFolder" isEqualToString:call.method]) { - if (self->ignoreCheckPermission) { - [self createFolder:call manager:manager handler:handler]; - return; - } - [self requestPermissionStatus:2 completeHandler:^(PHAuthorizationStatus status) { - if (status == PHAuthorizationStatusAuthorized) { - [self createFolder:call manager:manager handler:handler]; - return; - } - [handler reply:[FlutterError - errorWithCode:@"PERMISSION_NOT_AUTHORIZED" - message:@"fetchPathProperties only works with authorized permission." - details:nil]]; - }]; - } else if ([@"createAlbum" isEqualToString:call.method]) { - if (self->ignoreCheckPermission) { - [self createAlbum:call manager:manager handler:handler]; - return; - } - [self requestPermissionStatus:2 completeHandler:^(PHAuthorizationStatus status) { - if (status == PHAuthorizationStatusAuthorized) { - [self createAlbum:call manager:manager handler:handler]; - return; - } - [handler reply:[FlutterError - errorWithCode:@"PERMISSION_NOT_AUTHORIZED" - message:@"fetchPathProperties only works with authorized permission." - details:nil]]; - }]; - } else if ([@"removeInAlbum" isEqualToString:call.method]) { - NSArray *assetId = call.arguments[@"assetId"]; - NSString *pathId = call.arguments[@"pathId"]; - - [manager removeInAlbumWithAssetId:assetId albumId:pathId block:^(NSString *msg) { - if (msg) { - [handler reply:@{@"msg": msg}]; - } else { - [handler reply:@{@"success": @YES}]; - } - }]; - } else if ([@"deleteAlbum" isEqualToString:call.method]) { - NSString *id = call.arguments[@"id"]; - int type = [call.arguments[@"type"] intValue]; - [manager removeCollectionWithId:id type:type block:^(NSString *msg) { - if (msg) { - [handler reply:@{@"errorMsg": msg}]; - } else { - [handler reply:@{@"result": @YES}]; - } - }]; - } else if ([@"favoriteAsset" isEqualToString:call.method]) { - NSString *id = call.arguments[@"id"]; - BOOL favorite = [call.arguments[@"favorite"] boolValue]; - BOOL favoriteResult = [manager favoriteWithId:id favorite:favorite]; - [handler reply:@(favoriteResult)]; - } else if ([@"requestCacheAssetsThumb" isEqualToString:call.method]) { - NSArray *ids = call.arguments[@"ids"]; - PMThumbLoadOption *option = [PMThumbLoadOption optionDict:call.arguments[@"option"]]; - [manager requestCacheAssetsThumb:ids option:option]; - [handler reply:@YES]; - } else if ([@"cancelCacheRequests" isEqualToString:call.method]) { - [manager cancelCacheRequests]; - [handler reply:@YES]; - } else { - [handler notImplemented]; + if (!asset) { + [handler reply:nil]; + return; + } + [handler reply:[PMConvertUtils convertPMAssetToMap:asset needTitle:NO]]; + }]; + } else if ([call.method isEqualToString:@"assetExists"]) { + NSString *assetId = call.arguments[@"id"]; + BOOL exists = [manager existsWithId:assetId]; + [handler reply:@(exists)]; + } else if ([call.method isEqualToString:@"isLocallyAvailable"]) { + NSString *assetId = call.arguments[@"id"]; + BOOL isOrigin = [call.arguments[@"isOrigin"] boolValue]; + BOOL exists = [manager entityIsLocallyAvailable:assetId resource:nil isOrigin:isOrigin]; + [handler reply:@(exists)]; + } else if ([call.method isEqualToString:@"getTitleAsync"]) { + NSString *assetId = call.arguments[@"id"]; + int subtype = [call.arguments[@"subtype"] intValue]; + NSString *title = [manager getTitleAsyncWithAssetId:assetId subtype:subtype]; + [handler reply:title]; + } else if ([call.method isEqualToString:@"getMimeTypeAsync"]) { + NSString *assetId = call.arguments[@"id"]; + NSString *mimeType = [manager getMimeTypeAsyncWithAssetId:assetId]; + [handler reply:mimeType]; + } else if ([@"getMediaUrl" isEqualToString:call.method]) { + [manager getMediaUrl:call.arguments[@"id"] resultHandler:handler]; + } else if ([@"fetchEntityProperties" isEqualToString:call.method]) { + NSString *assetId = call.arguments[@"id"]; + PMAssetEntity *entity = [manager getAssetEntity:assetId withCache:NO]; + if (entity == nil) { + [handler reply:nil]; + return; } - }]; + [handler reply:[PMConvertUtils convertPMAssetToMap:entity needTitle:YES]]; + } else if ([@"getSubPath" isEqualToString:call.method]) { + NSString *galleryId = call.arguments[@"id"]; + int type = [call.arguments[@"type"] intValue]; + int albumType = [call.arguments[@"albumType"] intValue]; + NSDictionary *optionMap = call.arguments[@"option"]; + NSObject *option = [PMConvertUtils convertMapToOptionContainer:optionMap]; + + NSArray *array = [manager getSubPathWithId:galleryId type:type albumType:albumType option:option]; + NSDictionary *pathData = [PMConvertUtils convertPathToMap:array]; + + [handler reply:@{@"list": pathData}]; + } else if ([@"copyAsset" isEqualToString:call.method]) { + NSString *assetId = call.arguments[@"assetId"]; + NSString *galleryId = call.arguments[@"galleryId"]; + [manager copyAssetWithId:assetId toGallery:galleryId block:^(PMAssetEntity *entity, NSString *msg) { + if (msg) { + NSLog(@"copy asset error, cause by : %@", msg); + [handler reply:nil]; + } else { + [handler reply:[PMConvertUtils convertPMAssetToMap:entity needTitle:NO]]; + } + }]; + } else if ([@"createFolder" isEqualToString:call.method]) { + if (ignoreCheckPermission) { + [self createFolder:call manager:manager handler:handler]; + return; + } + [self requestPermissionStatus:2 completeHandler:^(PHAuthorizationStatus status) { + if (status == PHAuthorizationStatusAuthorized) { + [self createFolder:call manager:manager handler:handler]; + return; + } + [handler reply:[FlutterError + errorWithCode:@"PERMISSION_NOT_AUTHORIZED" + message:@"fetchPathProperties only works with authorized permission." + details:nil]]; + }]; + } else if ([@"createAlbum" isEqualToString:call.method]) { + if (ignoreCheckPermission) { + [self createAlbum:call manager:manager handler:handler]; + return; + } + [self requestPermissionStatus:2 completeHandler:^(PHAuthorizationStatus status) { + if (status == PHAuthorizationStatusAuthorized) { + [self createAlbum:call manager:manager handler:handler]; + return; + } + [handler reply:[FlutterError + errorWithCode:@"PERMISSION_NOT_AUTHORIZED" + message:@"fetchPathProperties only works with authorized permission." + details:nil]]; + }]; + } else if ([@"removeInAlbum" isEqualToString:call.method]) { + NSArray *assetId = call.arguments[@"assetId"]; + NSString *pathId = call.arguments[@"pathId"]; + + [manager removeInAlbumWithAssetId:assetId albumId:pathId block:^(NSString *msg) { + if (msg) { + [handler reply:@{@"msg": msg}]; + } else { + [handler reply:@{@"success": @YES}]; + } + }]; + } else if ([@"getAssetCount" isEqualToString:call.method]) { + int type = [call.arguments[@"type"] intValue]; + NSObject *option = + [PMConvertUtils convertMapToOptionContainer:call.arguments[@"option"]]; + NSUInteger count = [manager getAssetCountWithType:type option:option]; + [handler reply:@(count)]; + } else if ([@"getAssetsByRange" isEqualToString:call.method]) { + int type = [call.arguments[@"type"] intValue]; + int start = [call.arguments[@"start"] intValue]; + int end = [call.arguments[@"end"] intValue]; + NSObject *option = + [PMConvertUtils convertMapToOptionContainer:call.arguments[@"option"]]; + NSArray *array= [manager getAssetsWithType:type option: option start:start end:end]; + NSDictionary *resultDict = [PMConvertUtils convertAssetToMap:array optionGroup:option]; + [handler reply:resultDict]; + } else if ([@"deleteAlbum" isEqualToString:call.method]) { + NSString *id = call.arguments[@"id"]; + int type = [call.arguments[@"type"] intValue]; + [manager removeCollectionWithId:id type:type block:^(NSString *msg) { + if (msg) { + [handler reply:@{@"errorMsg": msg}]; + } else { + [handler reply:@{@"result": @YES}]; + } + }]; + } else if ([@"favoriteAsset" isEqualToString:call.method]) { + NSString *id = call.arguments[@"id"]; + BOOL favorite = [call.arguments[@"favorite"] boolValue]; + BOOL favoriteResult = [manager favoriteWithId:id favorite:favorite]; + [handler reply:@(favoriteResult)]; + } else if ([@"requestCacheAssetsThumb" isEqualToString:call.method]) { + NSArray *ids = call.arguments[@"ids"]; + PMThumbLoadOption *option = [PMThumbLoadOption optionDict:call.arguments[@"option"]]; + [manager requestCacheAssetsThumb:ids option:option]; + [handler reply:@YES]; + } else if ([@"cancelCacheRequests" isEqualToString:call.method]) { + [manager cancelCacheRequests]; + [handler reply:@YES]; + } else { + [handler notImplemented]; + } } - (NSDictionary *)convertToResult:(NSString *)id errorMsg:(NSString *)errorMsg { @@ -517,11 +541,11 @@ - (NSDictionary *)convertToResult:(NSString *)id errorMsg:(NSString *)errorMsg { if (errorMsg) { mutableDictionary[@"errorMsg"] = errorMsg; } - + if (id) { mutableDictionary[@"id"] = id; } - + return mutableDictionary; } @@ -533,7 +557,7 @@ - (PMProgressHandler *)getProgressHandlerFromDict:(NSDictionary *)dict { int index = [progressIndex intValue]; PMProgressHandler *handler = [PMProgressHandler new]; [handler register:privateRegistrar channelIndex:index]; - + return handler; } @@ -541,27 +565,27 @@ - (void)createFolder:(FlutterMethodCall *)call manager:(PMManager *)manager hand NSString *name = call.arguments[@"name"]; BOOL isRoot = [call.arguments[@"isRoot"] boolValue]; NSString *parentId = call.arguments[@"folderId"]; - + if (isRoot) { parentId = nil; } - + [manager createFolderWithName:name parentId:parentId block:^(NSString *id, NSString *errorMsg) { - [handler reply:[self convertToResult:id errorMsg:errorMsg]]; + [handler reply:[self convertToResult:id errorMsg:errorMsg]]; }]; } -- (void) createAlbum:(FlutterMethodCall *)call manager:(PMManager *)manager handler:(ResultHandler *)handler { +- (void)createAlbum:(FlutterMethodCall *)call manager:(PMManager *)manager handler:(ResultHandler *)handler { NSString *name = call.arguments[@"name"]; BOOL isRoot = [call.arguments[@"isRoot"] boolValue]; NSString *parentId = call.arguments[@"folderId"]; - + if (isRoot) { parentId = nil; } - + [manager createAlbumWithName:name parentId:parentId block:^(NSString *id, NSString *errorMsg) { - [handler reply:[self convertToResult:id errorMsg:errorMsg]]; + [handler reply:[self convertToResult:id errorMsg:errorMsg]]; }]; } diff --git a/ios/Classes/core/PMBaseFilter.h b/ios/Classes/core/PMBaseFilter.h new file mode 100644 index 00000000..a623700d --- /dev/null +++ b/ios/Classes/core/PMBaseFilter.h @@ -0,0 +1,15 @@ +// +// Created by jinglong cai on 2023/2/9. +// + +#import + +@protocol PMBaseFilter + +- (PHFetchOptions *)getFetchOptions:(int)type; + +- (BOOL) containsModified; + +- (BOOL) needTitle; + +@end diff --git a/ios/Classes/core/PMConvertUtils.h b/ios/Classes/core/PMConvertUtils.h index e568e063..9c55bee5 100644 --- a/ios/Classes/core/PMConvertUtils.h +++ b/ios/Classes/core/PMConvertUtils.h @@ -1,5 +1,7 @@ #import #import +#import "PMBaseFilter.h" + @class PMAssetPathEntity; @class PMAssetEntity; @class PMFilterOption; @@ -10,7 +12,7 @@ + (NSDictionary *)convertPathToMap:(NSArray *)array; + (NSDictionary *)convertAssetToMap:(NSArray *)array - optionGroup:(PMFilterOptionGroup *)optionGroup; + optionGroup:(NSObject *)optionGroup; + (NSDictionary *)convertPHAssetToMap:(PHAsset *)asset needTitle:(BOOL)needTitle; @@ -20,5 +22,5 @@ + (PMFilterOption *)convertMapToPMFilterOption:(NSDictionary *)map; -+ (PMFilterOptionGroup *)convertMapToOptionContainer:(NSDictionary *)map; ++ (NSObject *)convertMapToOptionContainer:(NSDictionary *)map; @end diff --git a/ios/Classes/core/PMConvertUtils.m b/ios/Classes/core/PMConvertUtils.m index b649e97b..f4c71a88 100644 --- a/ios/Classes/core/PMConvertUtils.m +++ b/ios/Classes/core/PMConvertUtils.m @@ -8,7 +8,7 @@ @implementation PMConvertUtils { + (NSDictionary *)convertPathToMap:(NSArray *)array { NSMutableArray *data = [NSMutableArray new]; - + for (PMAssetPathEntity *entity in array) { NSDictionary *item = @{ @"id": entity.id, @@ -16,10 +16,10 @@ + (NSDictionary *)convertPathToMap:(NSArray *)array { @"isAll": @(entity.isAll), @"albumType": @(entity.type), }; - + NSMutableDictionary *params = [NSMutableDictionary new]; [params addEntriesFromDictionary:item]; - + NSUInteger assetCount = entity.assetCount; if (assetCount == 0) { continue; @@ -30,24 +30,24 @@ + (NSDictionary *)convertPathToMap:(NSArray *)array { if (entity.modifiedDate != 0) { params[@"modified"] = @(entity.modifiedDate); } - + [data addObject:params]; } - + return @{@"data": data}; } + (NSDictionary *)convertAssetToMap:(NSArray *)array - optionGroup:(PMFilterOptionGroup *)optionGroup { + optionGroup:(NSObject *)optionGroup { NSMutableArray *data = [NSMutableArray new]; - - BOOL videoShowTitle = optionGroup.videoOption.needTitle; - BOOL imageShowTitle = optionGroup.imageOption.needTitle; - + + BOOL videoShowTitle = optionGroup.needTitle; + BOOL imageShowTitle = optionGroup.needTitle; + for (PMAssetEntity *asset in array) { - + NSDictionary *item; - + if ([asset.phAsset isImage]) { item = [PMConvertUtils convertPMAssetToMap:asset needTitle:imageShowTitle]; } else if ([asset.phAsset isVideo]) { @@ -57,7 +57,7 @@ + (NSDictionary *)convertAssetToMap:(NSArray *)array } [data addObject:item]; } - + return @{@"data": data}; } @@ -65,9 +65,9 @@ + (NSDictionary *)convertPHAssetToMap:(PHAsset *)asset needTitle:(BOOL)needTitle { long createDt = (int) asset.creationDate.timeIntervalSince1970; long modifiedDt = (int) asset.modificationDate.timeIntervalSince1970; - + int typeInt = 0; - + if (asset.isVideo) { typeInt = 2; } else if (asset.isImage) { @@ -75,7 +75,7 @@ + (NSDictionary *)convertPHAssetToMap:(PHAsset *)asset } else if (asset.isAudio) { typeInt = 3; } - + return @{ @"id": asset.localIdentifier, @"createDt": @(createDt), @@ -110,34 +110,46 @@ + (NSDictionary *)convertPMAssetToMap:(PMAssetEntity *)asset }; } -+ (PMFilterOptionGroup *)convertMapToOptionContainer:(NSDictionary *)map { - PMFilterOptionGroup *container = [PMFilterOptionGroup alloc]; - NSDictionary *image = map[@"image"]; - NSDictionary *video = map[@"video"]; - NSDictionary *audio = map[@"audio"]; - - container.imageOption = [self convertMapToPMFilterOption:image]; - container.videoOption = [self convertMapToPMFilterOption:video]; - container.audioOption = [self convertMapToPMFilterOption:audio]; - container.dateOption = [self convertMapToPMDateOption:map[@"createDate"]]; - container.updateOption = [self convertMapToPMDateOption:map[@"updateDate"]]; - container.containsModified = [map[@"containsPathModified"] boolValue]; - container.containsLivePhotos = [map[@"containsLivePhotos"] boolValue]; - container.onlyLivePhotos = [map[@"onlyLivePhotos"] boolValue]; - - NSArray *sortArray = map[@"orders"]; - [container injectSortArray: sortArray]; - - return container; ++ (NSObject *)convertMapToOptionContainer:(NSDictionary *)map { + int type = [map[@"type"] intValue]; + + if (type == 0) { + map = map[@"child"]; + + PMFilterOptionGroup *container = [PMFilterOptionGroup alloc]; + NSDictionary *image = map[@"image"]; + NSDictionary *video = map[@"video"]; + NSDictionary *audio = map[@"audio"]; + + container.imageOption = [self convertMapToPMFilterOption:image]; + container.videoOption = [self convertMapToPMFilterOption:video]; + container.audioOption = [self convertMapToPMFilterOption:audio]; + container.dateOption = [self convertMapToPMDateOption:map[@"createDate"]]; + container.updateOption = [self convertMapToPMDateOption:map[@"updateDate"]]; + container.containsModified = [map[@"containsPathModified"] boolValue]; + container.containsLivePhotos = [map[@"containsLivePhotos"] boolValue]; + container.onlyLivePhotos = [map[@"onlyLivePhotos"] boolValue]; + + NSArray *sortArray = map[@"orders"]; + [container injectSortArray:sortArray]; + + return container; + } else { + PMCustomFilterOption *option = [PMCustomFilterOption new]; + + option.params = map[@"child"]; + + return option; + } } + (PMFilterOption *)convertMapToPMFilterOption:(NSDictionary *)map { PMFilterOption *option = [PMFilterOption new]; option.needTitle = [map[@"title"] boolValue]; - + NSDictionary *sizeMap = map[@"size"]; NSDictionary *durationMap = map[@"duration"]; - + PMSizeConstraint sizeConstraint; sizeConstraint.minWidth = [sizeMap[@"minWidth"] unsignedIntValue]; sizeConstraint.maxWidth = [sizeMap[@"maxWidth"] unsignedIntValue]; @@ -145,28 +157,28 @@ + (PMFilterOption *)convertMapToPMFilterOption:(NSDictionary *)map { sizeConstraint.maxHeight = [sizeMap[@"maxHeight"] unsignedIntValue]; sizeConstraint.ignoreSize = [sizeMap[@"ignoreSize"] boolValue]; option.sizeConstraint = sizeConstraint; - + PMDurationConstraint durationConstraint; durationConstraint.minDuration = [PMConvertUtils convertNSNumberToSecond:durationMap[@"min"]]; durationConstraint.maxDuration = [PMConvertUtils convertNSNumberToSecond:durationMap[@"max"]]; durationConstraint.allowNullable = [durationMap[@"allowNullable"] boolValue]; option.durationConstraint = durationConstraint; - - + + return option; } + (PMDateOption *)convertMapToPMDateOption:(NSDictionary *)map { PMDateOption *option = [PMDateOption new]; - + long min = [map[@"min"] longValue]; long max = [map[@"max"] longValue]; BOOL ignore = [map[@"ignore"] boolValue]; - + option.min = [NSDate dateWithTimeIntervalSince1970:(min / 1000.0)]; option.max = [NSDate dateWithTimeIntervalSince1970:(max / 1000.0)]; option.ignore = ignore; - + return option; } diff --git a/ios/Classes/core/PMFilterOption.h b/ios/Classes/core/PMFilterOption.h index 26f9d1ef..a266023e 100644 --- a/ios/Classes/core/PMFilterOption.h +++ b/ios/Classes/core/PMFilterOption.h @@ -1,4 +1,5 @@ #import +#import "PMBaseFilter.h" @interface PMDateOption : NSObject @@ -14,19 +15,19 @@ typedef struct PMSizeConstraint { - unsigned int minWidth; - unsigned int maxWidth; - unsigned int minHeight; - unsigned int maxHeight; - BOOL ignoreSize; + unsigned int minWidth; + unsigned int maxWidth; + unsigned int minHeight; + unsigned int maxHeight; + BOOL ignoreSize; } PMSizeConstraint; typedef struct PMDurationConstraint { - double minDuration; - double maxDuration; - BOOL allowNullable; + double minDuration; + double maxDuration; + BOOL allowNullable; } PMDurationConstraint; @@ -46,7 +47,8 @@ typedef struct PMDurationConstraint { @end -@interface PMFilterOptionGroup : NSObject + +@interface PMFilterOptionGroup : NSObject @property(nonatomic, strong) PMFilterOption *imageOption; @property(nonatomic, strong) PMFilterOption *videoOption; @@ -56,9 +58,14 @@ typedef struct PMDurationConstraint { @property(nonatomic, assign) BOOL containsLivePhotos; @property(nonatomic, assign) BOOL onlyLivePhotos; @property(nonatomic, assign) BOOL containsModified; -@property(nonatomic, strong) NSArray *sortArray; +@property(nonatomic, strong) NSArray *sortArray; - (NSArray *)sortCond; - (void)injectSortArray:(NSArray *)array; @end + + +@interface PMCustomFilterOption : NSObject +@property (nonatomic, strong) NSDictionary *params; +@end \ No newline at end of file diff --git a/ios/Classes/core/PMFilterOption.m b/ios/Classes/core/PMFilterOption.m index 51de9a9b..11072504 100644 --- a/ios/Classes/core/PMFilterOption.m +++ b/ios/Classes/core/PMFilterOption.m @@ -1,6 +1,11 @@ +#import #import "PMFilterOption.h" +#import "PMRequestTypeUtils.h" +#import "NSString+PM_COMMON.h" +#import "PMLogUtils.h" @implementation PMFilterOptionGroup { + } - (NSArray *)sortCond { @@ -12,25 +17,25 @@ @implementation PMFilterOptionGroup { - (void)injectSortArray:(NSArray *)array { NSMutableArray *result = [NSMutableArray new]; - + // Handle platform default sorting first. if (array.count == 0) { // Set an empty sort array directly. self.sortArray = nil; return; } - + for (NSDictionary *dict in array) { int typeValue = [dict[@"type"] intValue]; BOOL asc = [dict[@"asc"] boolValue]; - + NSString *key = nil; if (typeValue == 0) { key = @"creationDate"; } else if (typeValue == 1) { key = @"modificationDate"; } - + if (key) { NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:key ascending:asc]; if (descriptor) { @@ -38,13 +43,148 @@ - (void)injectSortArray:(NSArray *)array { } } } - + self.sortArray = result; } + +- (PHFetchOptions *)getFetchOptions:(int)type { + PMFilterOptionGroup *optionGroup = self; + + PHFetchOptions *options = [PHFetchOptions new]; + options.sortDescriptors = [optionGroup sortCond]; + + NSMutableString *cond = [NSMutableString new]; + NSMutableArray *args = [NSMutableArray new]; + + BOOL containsImage = [PMRequestTypeUtils containsImage:type]; + BOOL containsVideo = [PMRequestTypeUtils containsVideo:type]; + BOOL containsAudio = [PMRequestTypeUtils containsAudio:type]; + + if (containsImage) { + [cond appendString:@" ( "]; + + PMFilterOption *imageOption = optionGroup.imageOption; + + NSString *sizeCond = [imageOption sizeCond]; + NSArray *sizeArgs = [imageOption sizeArgs]; + + [cond appendString:@"mediaType == %d"]; + [args addObject:@(PHAssetMediaTypeImage)]; + + if (!imageOption.sizeConstraint.ignoreSize) { + [cond appendString:@" AND "]; + [cond appendString:sizeCond]; + [args addObjectsFromArray:sizeArgs]; + } + if (@available(iOS 9.1, *)) { + if (optionGroup.onlyLivePhotos) { + [cond appendString:@" AND "]; + [cond appendString:[NSString + stringWithFormat:@"( ( mediaSubtype & %lu ) == 8 )", + (unsigned long) PHAssetMediaSubtypePhotoLive] + ]; + } else if (!optionGroup.containsLivePhotos) { + [cond appendString:@" AND "]; + [cond appendString:[NSString + stringWithFormat:@"NOT ( ( mediaSubtype & %lu ) == 8 )", + (unsigned long) PHAssetMediaSubtypePhotoLive] + ]; + } + } + + [cond appendString:@" )"]; + } + + if (containsVideo) { + if (![cond isEmpty]) { + [cond appendString:@" OR"]; + } + + [cond appendString:@" ( "]; + + PMFilterOption *videoOption = optionGroup.videoOption; + + [cond appendString:@"mediaType == %d"]; + [args addObject:@(PHAssetMediaTypeVideo)]; + + NSString *durationCond = [videoOption durationCond]; + NSArray *durationArgs = [videoOption durationArgs]; + [cond appendString:@" AND "]; + [cond appendString:durationCond]; + [args addObjectsFromArray:durationArgs]; + + if (@available(iOS 9.1, *)) { + if (!containsImage && optionGroup.containsLivePhotos) { + [cond appendString:@" )"]; + [cond appendString:@" OR "]; + [cond appendString:@"( "]; + [cond appendString:@"mediaType == %d"]; + [args addObject:@(PHAssetMediaTypeImage)]; + [cond appendString:@" AND "]; + [cond appendString:[NSString + stringWithFormat:@"( mediaSubtype & %lu ) == 8", + (unsigned long) PHAssetMediaSubtypePhotoLive] + ]; + } + } + + [cond appendString:@" ) "]; + } + + if (containsAudio) { + if (![cond isEmpty]) { + [cond appendString:@" OR "]; + } + + [cond appendString:@" ( "]; + + PMFilterOption *audioOption = optionGroup.audioOption; + + [cond appendString:@"mediaType == %d"]; + [args addObject:@(PHAssetMediaTypeAudio)]; + + NSString *durationCond = [audioOption durationCond]; + NSArray *durationArgs = [audioOption durationArgs]; + [cond appendString:@" AND "]; + [cond appendString:durationCond]; + [args addObjectsFromArray:durationArgs]; + + [PMLogUtils.sharedInstance info:[NSString stringWithFormat:@"duration = %.2f ~ %.2f", + [durationArgs[0] floatValue], + [durationArgs[1] floatValue]]]; + + [cond appendString:@" ) "]; + } + + [cond insertString:@"(" atIndex:0]; + [cond appendString:@")"]; + + PMDateOption *dateOption = optionGroup.dateOption; + if (!dateOption.ignore) { + [cond appendString:[dateOption dateCond:@"creationDate"]]; + [args addObjectsFromArray:[dateOption dateArgs]]; + } + + PMDateOption *updateOption = optionGroup.updateOption; + if (!updateOption.ignore) { + [cond appendString:[updateOption dateCond:@"modificationDate"]]; + [args addObjectsFromArray:[updateOption dateArgs]]; + } + + options.predicate = [NSPredicate predicateWithFormat:cond argumentArray:args]; + + return options; +} + +- (BOOL)needTitle { + return self.videoOption.needTitle || self.imageOption.needTitle; +} + + @end @implementation PMFilterOption { - + } - (NSString *)sizeCond { return @"pixelWidth >=%d AND pixelWidth <=%d AND pixelHeight >=%d AND pixelHeight <=%d"; @@ -73,31 +213,31 @@ - (NSArray *)durationArgs { @implementation PMDateOption { - + } - (NSString *)dateCond:(NSString *)key { NSMutableString *str = [NSMutableString new]; - + [str appendString:@" AND "]; [str appendString:@"( "]; - + // min - + [str appendString:key]; [str appendString:@" >= %@"]; - - + + // and [str appendString:@" AND "]; - + // max - + [str appendString:key]; [str appendString:@" <= %@ "]; - + [str appendString:@") "]; - + return str; } @@ -106,3 +246,80 @@ - (NSArray *)dateArgs { } @end + +@implementation PMCustomFilterOption { + +} + +- (NSString *)where { + return self.params[@"where"]; +} + +- (NSArray *)sortDescriptors { + NSMutableArray *sortDescriptors = [NSMutableArray new]; + NSArray *array = self.params[@"orderBy"]; + + for (NSDictionary *dict in array) { + NSString *column = dict[@"column"]; + BOOL ascending = [dict[@"isAsc"] boolValue]; + [sortDescriptors addObject:[NSSortDescriptor sortDescriptorWithKey:column ascending:ascending]]; + } + + return sortDescriptors; +} + +- (PHFetchOptions *)getFetchOptions:(int)type { + PHFetchOptions *options = [PHFetchOptions new]; + + BOOL containsImage = [PMRequestTypeUtils containsImage:type]; + BOOL containsVideo = [PMRequestTypeUtils containsVideo:type]; + BOOL containsAudio = [PMRequestTypeUtils containsAudio:type]; + + NSMutableString *typeWhere = [NSMutableString new]; + + if (containsImage) { + if (!typeWhere.isEmpty) { + [typeWhere appendString:@" OR "]; + } + + [typeWhere appendFormat:@"mediaType == %ld", PHAssetMediaTypeImage]; + } + if (containsVideo) { + if (!typeWhere.isEmpty) { + [typeWhere appendString:@" OR "]; + } + + [typeWhere appendFormat:@"mediaType == %ld", PHAssetMediaTypeVideo]; + } + if (containsAudio) { + if (!typeWhere.isEmpty) { + [typeWhere appendString:@" OR "]; + } + + [typeWhere appendFormat:@"mediaType == %ld", PHAssetMediaTypeAudio]; + } + + NSString *where = [self where]; + if (!where.isEmpty) { + NSString *text = [NSString stringWithFormat:@"%@ AND ( %@ )", where, typeWhere]; + NSPredicate *predicate = [NSPredicate predicateWithFormat: text]; + options.predicate = predicate; + } else { + NSPredicate *predicate = [NSPredicate predicateWithFormat: typeWhere]; + options.predicate = predicate; + } + + options.sortDescriptors = [self sortDescriptors]; + + return options; +} + +- (BOOL)containsModified { + return [self.params[@"containsPathModified"] boolValue]; +} + +- (BOOL)needTitle { + return NO; +} + +@end diff --git a/ios/Classes/core/PMManager.h b/ios/Classes/core/PMManager.h index 8b89db50..eeadaf77 100644 --- a/ios/Classes/core/PMManager.h +++ b/ios/Classes/core/PMManager.h @@ -33,13 +33,13 @@ typedef void (^AssetResult)(PMAssetEntity *); - (void)setAuth:(BOOL)auth; -- (NSArray *)getAssetPathList:(int)type hasAll:(BOOL)hasAll onlyAll:(BOOL)onlyAll option:(PMFilterOptionGroup *)option; +- (NSArray *)getAssetPathList:(int)type hasAll:(BOOL)hasAll onlyAll:(BOOL)onlyAll option:(NSObject *)option; -- (NSUInteger)getAssetCountFromPath:(NSString *)id type:(int)type filterOption:(PMFilterOptionGroup *)filterOption; +- (NSUInteger)getAssetCountFromPath:(NSString *)id type:(int)type filterOption:(NSObject *)filterOption; -- (NSArray *)getAssetListPaged:(NSString *)id type:(int)type page:(NSUInteger)page size:(NSUInteger)size filterOption:(PMFilterOptionGroup *)filterOption; +- (NSArray *)getAssetListPaged:(NSString *)id type:(int)type page:(NSUInteger)page size:(NSUInteger)size filterOption:(NSObject *)filterOption; -- (NSArray *)getAssetListRange:(NSString *)id type:(int)type start:(NSUInteger)start end:(NSUInteger)end filterOption:(PMFilterOptionGroup *)filterOption; +- (NSArray *)getAssetListRange:(NSString *)id type:(int)type start:(NSUInteger)start end:(NSUInteger)end filterOption:(NSObject *)filterOption; - (PMAssetEntity *)getAssetEntity:(NSString *)assetId; @@ -51,7 +51,7 @@ typedef void (^AssetResult)(PMAssetEntity *); - (void)getFullSizeFileWithId:(NSString *)id isOrigin:(BOOL)isOrigin subtype:(int)subtype resultHandler:(NSObject *)handler progressHandler:(NSObject *)progressHandler; -- (PMAssetPathEntity *)fetchPathProperties:(NSString *)id type:(int)type filterOption:(PMFilterOptionGroup *)filterOption; +- (PMAssetPathEntity *)fetchPathProperties:(NSString *)id type:(int)type filterOption:(NSObject *)filterOption; - (void)deleteWithIds:(NSArray *)ids changedBlock:(ChangeIds)block; @@ -81,7 +81,7 @@ typedef void (^AssetResult)(PMAssetEntity *); - (void)getMediaUrl:(NSString *)assetId resultHandler:(NSObject *)handler; -- (NSArray *)getSubPathWithId:(NSString *)id type:(int)type albumType:(int)albumType option:(PMFilterOptionGroup *)option; +- (NSArray *)getSubPathWithId:(NSString *)id type:(int)type albumType:(int)albumType option:(NSObject *)option; - (void)saveImageWithPath:(NSString *)path title:(NSString *)title desc:(NSString *)desc block:(void (^)(PMAssetEntity *))block; @@ -104,4 +104,9 @@ typedef void (^AssetResult)(PMAssetEntity *); - (void)cancelCacheRequests; - (void)injectModifyToDate:(PMAssetPathEntity *)path; + +- (NSUInteger) getAssetCountWithType:(int)type option:(NSObject *) filter; + +- (NSArray*) getAssetsWithType:(int)type option:(NSObject *)option start:(int)start end:(int)end; + @end diff --git a/ios/Classes/core/PMManager.m b/ios/Classes/core/PMManager.m index 9cfc61d4..ae3689de 100644 --- a/ios/Classes/core/PMManager.m +++ b/ios/Classes/core/PMManager.m @@ -17,7 +17,7 @@ @implementation PMManager { BOOL __isAuth; PMCacheContainer *cacheContainer; - + PHCachingImageManager *__cachingManager; } @@ -27,7 +27,7 @@ - (instancetype)init { __isAuth = NO; cacheContainer = [PMCacheContainer new]; } - + return self; } @@ -43,15 +43,15 @@ - (PHCachingImageManager *)cachingManager { if (__cachingManager == nil) { __cachingManager = [PHCachingImageManager new]; } - + return __cachingManager; } -- (NSArray *)getAssetPathList:(int)type hasAll:(BOOL)hasAll onlyAll:(BOOL)onlyAll option:(PMFilterOptionGroup *)option { +- (NSArray *)getAssetPathList:(int)type hasAll:(BOOL)hasAll onlyAll:(BOOL)onlyAll option:(NSObject *)option { NSMutableArray *array = [NSMutableArray new]; PHFetchOptions *assetOptions = [self getAssetOptions:type filterOption:option]; PHFetchOptions *fetchCollectionOptions = [PHFetchOptions new]; - + PHFetchResult *smartAlbumResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAny @@ -77,7 +77,7 @@ - (PHCachingImageManager *)cachingManager { options:assetOptions hasAll:hasAll containsModified:option.containsModified]; - + PHFetchResult *albumResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny @@ -88,17 +88,17 @@ - (PHCachingImageManager *)cachingManager { options:assetOptions hasAll:hasAll containsModified:option.containsModified]; - + return array; } -- (NSUInteger)getAssetCountFromPath:(NSString *)id type:(int)type filterOption:(PMFilterOptionGroup *)filterOption { +- (NSUInteger)getAssetCountFromPath:(NSString *)id type:(int)type filterOption:(NSObject *)filterOption { PHFetchOptions *assetOptions = [self getAssetOptions:type filterOption:filterOption]; PHFetchOptions *fetchCollectionOptions = [PHFetchOptions new]; PHFetchResult *result = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[id] options:fetchCollectionOptions]; - + if (result == nil || result.count == 0) { return 0; } @@ -122,6 +122,35 @@ - (void)logCollections:(PHFetchResult *)collections option:(PHFetchOptions *)opt } } +- (NSUInteger)getAssetCountWithType:(int)type option:(NSObject *)filter { + PHFetchOptions *options = [filter getFetchOptions:type]; + PHFetchResult *result = [PHAsset fetchAssetsWithOptions:options]; + return result.count; +} + +- (NSArray *)getAssetsWithType:(int)type option:(NSObject *)option start:(int)start end:(int)end { + PHFetchOptions *options = [option getFetchOptions:type]; + PHFetchResult *result = [PHAsset fetchAssetsWithOptions:options]; + + NSUInteger endOffset = end; + if (endOffset < result.count) { + endOffset = result.count; + } + + NSMutableArray* array = [NSMutableArray new]; + + for (NSUInteger i = start; i < endOffset; i++){ + if (i >= result.count) { + break; + } + PHAsset *asset = result[i]; + PMAssetEntity *pmAsset = [self convertPHAssetToAssetEntity:asset needTitle:[option needTitle]]; + [array addObject: pmAsset]; + } + + return array; +} + - (BOOL)existsWithId:(NSString *)assetId { PHFetchResult *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[PHFetchOptions new]]; @@ -161,25 +190,25 @@ - (void)injectAssetPathIntoArray:(NSMutableArray *)array if (![collection isKindOfClass:[PHAssetCollection class]]) { continue; } - + PHAssetCollection *assetCollection = (PHAssetCollection *) collection; - + // Check whether it's "Recently Deleted" if (assetCollection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumRecentlyAdded || assetCollection.assetCollectionSubtype == 1000000201) { continue; } - + // Check nullable id and name NSString *localIdentifier = assetCollection.localIdentifier; NSString *localizedTitle = assetCollection.localizedTitle; if (!localIdentifier || localIdentifier.isEmpty || !localizedTitle || localizedTitle.isEmpty) { continue; } - + PMAssetPathEntity *entity = [PMAssetPathEntity entityWithId:localIdentifier name:localizedTitle]; entity.isAll = assetCollection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumUserLibrary; - + if (containsModified) { PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:options]; entity.assetCount = fetchResult.count; @@ -188,7 +217,7 @@ - (void)injectAssetPathIntoArray:(NSMutableArray *)array entity.modifiedDate = (long) asset.modificationDate.timeIntervalSince1970; } } - + if (!hasAll && entity.isAll) { continue; } @@ -198,38 +227,38 @@ - (void)injectAssetPathIntoArray:(NSMutableArray *)array #pragma clang diagnostic pop -- (NSArray *)getAssetListPaged:(NSString *)id type:(int)type page:(NSUInteger)page size:(NSUInteger)size filterOption:(PMFilterOptionGroup *)filterOption { +- (NSArray *)getAssetListPaged:(NSString *)id type:(int)type page:(NSUInteger)page size:(NSUInteger)size filterOption:(NSObject *)filterOption { NSMutableArray *result = [NSMutableArray new]; - + PHFetchOptions *options = [PHFetchOptions new]; - + PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[id] options:options]; if (fetchResult && fetchResult.count == 0) { return result; } - + PHAssetCollection *collection = fetchResult.firstObject; PHFetchOptions *assetOptions = [self getAssetOptions:type filterOption:filterOption]; PHFetchResult *assetArray = [PHAsset fetchAssetsInAssetCollection:collection options:assetOptions]; - + if (assetArray.count == 0) { return result; } - + NSUInteger startIndex = page * size; NSUInteger endIndex = startIndex + size - 1; - + NSUInteger count = assetArray.count; if (endIndex >= count) { endIndex = count - 1; } - - BOOL imageNeedTitle = filterOption.imageOption.needTitle; - BOOL videoNeedTitle = filterOption.videoOption.needTitle; - + + BOOL imageNeedTitle = filterOption.needTitle; + BOOL videoNeedTitle = filterOption.needTitle; + for (NSUInteger i = startIndex; i <= endIndex; i++) { NSUInteger index = i; if (assetOptions.sortDescriptors == nil) { @@ -246,39 +275,39 @@ - (void)injectAssetPathIntoArray:(NSMutableArray *)array [result addObject:entity]; [cacheContainer putAssetEntity:entity]; } - + return result; } -- (NSArray *)getAssetListRange:(NSString *)id type:(int)type start:(NSUInteger)start end:(NSUInteger)end filterOption:(PMFilterOptionGroup *)filterOption { +- (NSArray *)getAssetListRange:(NSString *)id type:(int)type start:(NSUInteger)start end:(NSUInteger)end filterOption:(NSObject *)filterOption { NSMutableArray *result = [NSMutableArray new]; - + PHFetchOptions *options = [PHFetchOptions new]; - + PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[id] options:options]; if (fetchResult && fetchResult.count == 0) { return result; } - + PHAssetCollection *collection = fetchResult.firstObject; PHFetchOptions *assetOptions = [self getAssetOptions:(int) type filterOption:filterOption]; PHFetchResult *assetArray = [PHAsset fetchAssetsInAssetCollection:collection options:assetOptions]; - + if (assetArray.count == 0) { return result; } - + NSUInteger startIndex = start; NSUInteger endIndex = end - 1; - + NSUInteger count = assetArray.count; if (endIndex >= count) { endIndex = count - 1; } - + for (NSUInteger i = startIndex; i <= endIndex; i++) { NSUInteger index = i; if (assetOptions.sortDescriptors == nil) { @@ -287,18 +316,18 @@ - (void)injectAssetPathIntoArray:(NSMutableArray *)array PHAsset *asset = assetArray[index]; BOOL needTitle; if ([asset isVideo]) { - needTitle = filterOption.videoOption.needTitle; + needTitle = filterOption.needTitle; } else if ([asset isImage]) { - needTitle = filterOption.imageOption.needTitle; + needTitle = filterOption.needTitle; } else { needTitle = NO; } - + PMAssetEntity *entity = [self convertPHAssetToAssetEntity:asset needTitle:needTitle]; [result addObject:entity]; [cacheContainer putAssetEntity:entity]; } - + return result; } @@ -306,20 +335,20 @@ - (PMAssetEntity *)convertPHAssetToAssetEntity:(PHAsset *)asset needTitle:(BOOL)needTitle { // type: // 0: all , 1: image, 2:video - + int type = 0; if (asset.isImage) { type = 1; } else if (asset.isVideo) { type = 2; } - + NSDate *date = asset.creationDate; long createDt = (long) date.timeIntervalSince1970; - + NSDate *modifiedDate = asset.modificationDate; long modifiedTimeStamp = (long) modifiedDate.timeIntervalSince1970; - + PMAssetEntity *entity = [PMAssetEntity entityWithId:asset.localIdentifier createDt:createDt width:asset.pixelWidth @@ -333,7 +362,7 @@ - (PMAssetEntity *)convertPHAssetToAssetEntity:(PHAsset *)asset entity.title = needTitle ? [asset title] : @""; entity.favorite = asset.isFavorite; entity.subtype = asset.mediaSubtypes; - + return entity; } @@ -354,7 +383,7 @@ - (PMAssetEntity *)getAssetEntity:(NSString *)assetId withCache:(BOOL)withCache if (result == nil || result.count == 0) { return nil; } - + PHAsset *asset = result[0]; entity = [self convertPHAssetToAssetEntity:asset needTitle:NO]; [cacheContainer putAssetEntity:entity]; @@ -380,9 +409,9 @@ - (void)fetchThumb:(PHAsset *)asset option:(PMThumbLoadOption *)option resultHan PHImageRequestOptions *requestOptions = [PHImageRequestOptions new]; requestOptions.deliveryMode = option.deliveryMode; requestOptions.resizeMode = option.resizeMode; - + [self notifyProgress:progressHandler progress:0 state:PMProgressStatePrepare]; - + [requestOptions setNetworkAccessAllowed:YES]; [requestOptions setProgressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { @@ -397,18 +426,18 @@ - (void)fetchThumb:(PHAsset *)asset option:(PMThumbLoadOption *)option resultHan }]; int width = option.width; int height = option.height; - + [manager requestImageForAsset:asset targetSize:CGSizeMake(width, height) contentMode:option.contentMode options:requestOptions resultHandler:^(PMImage *result, NSDictionary *info) { BOOL downloadFinished = [PMManager isDownloadFinish:info]; - + if (!downloadFinished) { return; } - + if ([handler isReplied]) { return; } @@ -419,11 +448,11 @@ - (void)fetchThumb:(PHAsset *)asset option:(PMThumbLoadOption *)option resultHan } else { [handler reply: nil]; } - + [self notifySuccess:progressHandler]; - + }]; - + } - (void)getFullSizeFileWithId:(NSString *)id isOrigin:(BOOL)isOrigin subtype:(int)subtype resultHandler:(NSObject *)handler progressHandler:(NSObject *)progressHandler { @@ -467,17 +496,17 @@ - (void)fetchLivePhotosFile:(PHAsset *)asset handler:(NSObject [handler reply:path]; return; } - + PHAssetResourceRequestOptions *options = [PHAssetResourceRequestOptions new]; [options setNetworkAccessAllowed:YES]; - + [self notifyProgress:progressHandler progress:0 state:PMProgressStatePrepare]; [options setProgressHandler:^(double progress) { if (progress != 1) { [self notifyProgress:progressHandler progress:progress state:PMProgressStateLoading]; } }]; - + PHAssetResourceManager *resourceManager = PHAssetResourceManager.defaultManager; NSURL *fileUrl = [NSURL fileURLWithPath:path]; [resourceManager writeDataForAssetResource:resource @@ -508,17 +537,17 @@ - (void)fetchOriginVideoFile:(PHAsset *)asset handler:(NSObject *)handler progressHandler:(NSObject *)progressHandler { PHImageManager *manager = PHImageManager.defaultManager; - + PHImageRequestOptions *options = [PHImageRequestOptions new]; [options setDeliveryMode:PHImageRequestOptionsDeliveryModeOpportunistic]; [options setNetworkAccessAllowed:YES]; @@ -768,32 +797,32 @@ - (void)fetchFullSizeImageFile:(PHAsset *)asset resultHandler:(NSObject *)filterOption { PHFetchOptions *collectionFetchOptions = [PHFetchOptions new]; PHFetchResult *result = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[id] options:collectionFetchOptions]; - + if (result == nil || result.count == 0) { return nil; } PHAssetCollection *collection = result[0]; - + // Check nullable id and name NSString *localIdentifier = collection.localIdentifier; NSString *localizedTitle = collection.localizedTitle; @@ -827,131 +856,8 @@ - (PMAssetPathEntity *)fetchPathProperties:(NSString *)id type:(int)type filterO return entity; } -- (PHFetchOptions *)getAssetOptions:(int)type filterOption:(PMFilterOptionGroup *)optionGroup { - PHFetchOptions *options = [PHFetchOptions new]; - options.sortDescriptors = [optionGroup sortCond]; - - NSMutableString *cond = [NSMutableString new]; - NSMutableArray *args = [NSMutableArray new]; - - BOOL containsImage = [PMRequestTypeUtils containsImage:type]; - BOOL containsVideo = [PMRequestTypeUtils containsVideo:type]; - BOOL containsAudio = [PMRequestTypeUtils containsAudio:type]; - - if (containsImage) { - [cond appendString:@" ( "]; - - PMFilterOption *imageOption = optionGroup.imageOption; - - NSString *sizeCond = [imageOption sizeCond]; - NSArray *sizeArgs = [imageOption sizeArgs]; - - [cond appendString:@"mediaType == %d"]; - [args addObject:@(PHAssetMediaTypeImage)]; - - if (!imageOption.sizeConstraint.ignoreSize) { - [cond appendString:@" AND "]; - [cond appendString:sizeCond]; - [args addObjectsFromArray:sizeArgs]; - } - if (@available(iOS 9.1, *)) { - if (optionGroup.onlyLivePhotos) { - [cond appendString:@" AND "]; - [cond appendString:[NSString - stringWithFormat:@"( ( mediaSubtype & %lu ) == 8 )", - (unsigned long)PHAssetMediaSubtypePhotoLive] - ]; - } else if (!optionGroup.containsLivePhotos) { - [cond appendString:@" AND "]; - [cond appendString:[NSString - stringWithFormat:@"NOT ( ( mediaSubtype & %lu ) == 8 )", - (unsigned long)PHAssetMediaSubtypePhotoLive] - ]; - } - } - - [cond appendString:@" )"]; - } - - if (containsVideo) { - if (![cond isEmpty]) { - [cond appendString:@" OR"]; - } - - [cond appendString:@" ( "]; - - PMFilterOption *videoOption = optionGroup.videoOption; - - [cond appendString:@"mediaType == %d"]; - [args addObject:@(PHAssetMediaTypeVideo)]; - - NSString *durationCond = [videoOption durationCond]; - NSArray *durationArgs = [videoOption durationArgs]; - [cond appendString:@" AND "]; - [cond appendString:durationCond]; - [args addObjectsFromArray:durationArgs]; - - if (@available(iOS 9.1, *)) { - if (!containsImage && optionGroup.containsLivePhotos) { - [cond appendString:@" )"]; - [cond appendString:@" OR "]; - [cond appendString:@"( "]; - [cond appendString:@"mediaType == %d"]; - [args addObject:@(PHAssetMediaTypeImage)]; - [cond appendString:@" AND "]; - [cond appendString:[NSString - stringWithFormat:@"( mediaSubtype & %lu ) == 8", - (unsigned long)PHAssetMediaSubtypePhotoLive] - ]; - } - } - - [cond appendString:@" ) "]; - } - - if (containsAudio) { - if (![cond isEmpty]) { - [cond appendString:@" OR "]; - } - - [cond appendString:@" ( "]; - - PMFilterOption *audioOption = optionGroup.audioOption; - - [cond appendString:@"mediaType == %d"]; - [args addObject:@(PHAssetMediaTypeAudio)]; - - NSString *durationCond = [audioOption durationCond]; - NSArray *durationArgs = [audioOption durationArgs]; - [cond appendString:@" AND "]; - [cond appendString:durationCond]; - [args addObjectsFromArray:durationArgs]; - - [PMLogUtils.sharedInstance info: [NSString stringWithFormat: @"duration = %.2f ~ %.2f", - [durationArgs[0] floatValue], - [durationArgs[1] floatValue]]]; - - [cond appendString:@" ) "]; - } - - [cond insertString:@"(" atIndex:0]; - [cond appendString:@")"]; - - PMDateOption *dateOption = optionGroup.dateOption; - if (!dateOption.ignore) { - [cond appendString:[dateOption dateCond:@"creationDate"]]; - [args addObjectsFromArray:[dateOption dateArgs]]; - } - - PMDateOption *updateOption = optionGroup.updateOption; - if (!updateOption.ignore) { - [cond appendString:[updateOption dateCond:@"modificationDate"]]; - [args addObjectsFromArray:[updateOption dateArgs]]; - } - - options.predicate = [NSPredicate predicateWithFormat:cond argumentArray:args]; - - return options; +- (PHFetchOptions *)getAssetOptions:(int)type filterOption:(NSObject *)optionGroup { + return [optionGroup getFetchOptions:type]; } #pragma clang diagnostic push @@ -1013,7 +919,7 @@ - (void)saveImage:(NSData *)data block:(AssetResult)block { __block NSString *assetId = nil; [PMLogUtils.sharedInstance info:[NSString stringWithFormat:@"save image with data, length: %lu, title:%@, desc: %@", (unsigned long)data.length, title, desc]]; - + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ PHAssetCreationRequest *request = @@ -1039,7 +945,7 @@ - (void)saveImage:(NSData *)data - (void)saveImageWithPath:(NSString *)path title:(NSString *)title desc:(NSString *)desc block:(void (^)(PMAssetEntity *))block { [PMLogUtils.sharedInstance info:[NSString stringWithFormat:@"save image with path: %@ title:%@, desc: %@", path, title, desc]]; - + __block NSString *assetId = nil; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ @@ -1162,29 +1068,29 @@ - (void)getMediaUrl:(NSString *)assetId resultHandler:(NSObject *)getSubPathWithId:(NSString *)id type:(int)type albumType:(int)albumType option:(PMFilterOptionGroup *)option { +- (NSArray *)getSubPathWithId:(NSString *)id type:(int)type albumType:(int)albumType option:(NSObject *)option { PHFetchOptions *options = [self getAssetOptions:type filterOption:option]; - + if ([PMFolderUtils isRecentCollection:id]) { NSArray *array = [PMFolderUtils getRootFolderWithOptions:nil]; return [self convertPHCollectionToPMAssetPathArray:array option:options]; } - + if (albumType == PM_TYPE_ALBUM) { return @[]; } - + PHCollectionList *list; - + PHFetchResult *collectionList = [PHCollectionList fetchCollectionListsWithLocalIdentifiers:@[id] options:nil]; if (collectionList && collectionList.count > 0) { list = collectionList.firstObject; } - + if (!list) { return @[]; } - + NSArray *phCollectionArray = [PMFolderUtils getSubCollectionWithCollection:list options:options]; return [self convertPHCollectionToPMAssetPathArray:phCollectionArray option:options]; } @@ -1192,17 +1098,17 @@ - (void)getMediaUrl:(NSString *)assetId resultHandler:(NSObject *)convertPHCollectionToPMAssetPathArray:(NSArray *)phArray option:(PHFetchOptions *)option { NSMutableArray *result = [NSMutableArray new]; - + for (PHCollection *collection in phArray) { [result addObject:[self convertPHCollectionToPMPath:collection option:option]]; } - + return result; } - (PMAssetPathEntity *)convertPHCollectionToPMPath:(PHCollection *)phCollection option:(PHFetchOptions *)option { PMAssetPathEntity *pathEntity = [PMAssetPathEntity new]; - + pathEntity.id = phCollection.localIdentifier; pathEntity.isAll = NO; pathEntity.name = phCollection.localizedTitle; @@ -1211,13 +1117,13 @@ - (PMAssetPathEntity *)convertPHCollectionToPMPath:(PHCollection *)phCollection } else { pathEntity.type = PM_TYPE_FOLDER; } - + return pathEntity; } - (PHAssetCollection *)getCollectionWithId:(NSString *)galleryId { PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[galleryId] options:nil]; - + if (fetchResult && fetchResult.count > 0) { return fetchResult.firstObject; } @@ -1226,42 +1132,42 @@ - (PHAssetCollection *)getCollectionWithId:(NSString *)galleryId { - (void)copyAssetWithId:(NSString *)id toGallery:(NSString *)gallery block:(void (^)(PMAssetEntity *entity, NSString *msg))block { PMAssetEntity *assetEntity = [self getAssetEntity:id]; - + if (!assetEntity) { NSString *msg = [NSString stringWithFormat:@"not found asset : %@", id]; block(nil, msg); return; } - + __block PHAssetCollection *collection = [self getCollectionWithId:gallery]; - + if (!collection) { NSString *msg = [NSString stringWithFormat:@"not found collection with gallery id : %@", gallery]; block(nil, msg); return; } - + if (![collection canPerformEditOperation:PHCollectionEditOperationAddContent]) { block(nil, @"The collection can't add from user. The [collection canPerformEditOperation:PHCollectionEditOperationAddContent] return NO!"); return; } - + __block PHFetchResult *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:nil]; NSError *error; - + [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:collection]; [request addAssets:asset]; - + } error:&error]; - + if (error) { NSString *msg = [NSString stringWithFormat:@"Can't copy, error : %@ ", error]; block(nil, msg); return; } - + block(assetEntity, nil); } @@ -1272,32 +1178,32 @@ - (void)createFolderWithName:(NSString *)name parentId:(NSString *)id block:(voi PHFetchResult *result = [PHCollectionList fetchCollectionListsWithLocalIdentifiers:@[id] options:nil]; if (result && result.count > 0) { PHCollectionList *parent = result.firstObject; - + [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ PHCollectionListChangeRequest *request = [PHCollectionListChangeRequest creationRequestForCollectionListWithTitle:name]; targetId = request.placeholderForCreatedCollectionList.localIdentifier; } error:&error]; - + if (error) { NSLog(@"createFolderWithName 1: error : %@", error); } - + [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ PHCollectionListChangeRequest *request = [PHCollectionListChangeRequest changeRequestForCollectionList:parent]; PHFetchResult *fetchResult = [PHCollectionList fetchCollectionListsWithLocalIdentifiers:@[targetId] options:nil]; [request addChildCollections:fetchResult]; } error:&error]; - - + + if (error) { NSLog(@"createFolderWithName 2: error : %@", error); } - - + + block(targetId, error.localizedDescription); - + } else { block(nil, [NSString stringWithFormat:@"Cannot find folder : %@", id]); return; @@ -1308,13 +1214,13 @@ - (void)createFolderWithName:(NSString *)name parentId:(NSString *)id block:(voi PHCollectionListChangeRequest *request = [PHCollectionListChangeRequest creationRequestForCollectionListWithTitle:name]; targetId = request.placeholderForCreatedCollectionList.localIdentifier; } error:&error]; - + if (error) { NSLog(@"createFolderWithName 3: error : %@", error); } block(targetId, error.localizedDescription); } - + } - (void)createAlbumWithName:(NSString *)name parentId:(NSString *)id block:(void (^)(NSString *, NSString *))block { @@ -1324,30 +1230,30 @@ - (void)createAlbumWithName:(NSString *)name parentId:(NSString *)id block:(void PHFetchResult *result = [PHCollectionList fetchCollectionListsWithLocalIdentifiers:@[id] options:nil]; if (result && result.count > 0) { PHCollectionList *parent = result.firstObject; - + [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:name]; targetId = request.placeholderForCreatedAssetCollection.localIdentifier; } error:&error]; - + if (error) { NSLog(@"createAlbumWithName 1: error : %@", error); } - + [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ PHCollectionListChangeRequest *request = [PHCollectionListChangeRequest changeRequestForCollectionList:parent]; PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[targetId] options:nil]; [request addChildCollections:fetchResult]; } error:&error]; - + if (error) { NSLog(@"createAlbumWithName 2: error : %@", error); } - + block(targetId, error.localizedDescription); - + } else { block(nil, [NSString stringWithFormat:@"Cannot find folder : %@", id]); return; @@ -1358,7 +1264,7 @@ - (void)createAlbumWithName:(NSString *)name parentId:(NSString *)id block:(void PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:name]; targetId = request.placeholderForCreatedAssetCollection.localIdentifier; } error:&error]; - + if (error) { NSLog(@"createAlbumWithName 3: error : %@", error); } @@ -1375,12 +1281,12 @@ - (void)removeInAlbumWithAssetId:(NSArray *)id albumId:(NSString *)albumId block block(@"Can't found the collection."); return; } - + if (![collection canPerformEditOperation:PHCollectionEditOperationRemoveContent]) { block(@"The collection cannot remove asset by user."); return; } - + PHFetchResult *assetResult = [PHAsset fetchAssetsWithLocalIdentifiers:id options:nil]; NSError *error; [PHPhotoLibrary.sharedPhotoLibrary @@ -1392,7 +1298,7 @@ - (void)removeInAlbumWithAssetId:(NSArray *)id albumId:(NSString *)albumId block block([NSString stringWithFormat:@"Remove error: %@", error]); return; } - + block(nil); } @@ -1419,14 +1325,14 @@ - (void)removeCollectionWithId:(NSString *)id type:(int)type block:(void (^)(NSS [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ [PHAssetCollectionChangeRequest deleteAssetCollections:@[collection]]; } error:&error]; - + if (error) { block([NSString stringWithFormat:@"Remove error: %@", error]); return; } - + block(nil); - + } else if (type == 2) { PHFetchResult *fetchResult = [PHCollectionList fetchCollectionListsWithLocalIdentifiers:@[id] options:nil]; PHCollectionList *collection = [self getFirstObjFromFetchResult:fetchResult]; @@ -1442,12 +1348,12 @@ - (void)removeCollectionWithId:(NSString *)id type:(int)type block:(void (^)(NSS [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ [PHCollectionListChangeRequest deleteCollectionLists:@[collection]]; } error:&error]; - + if (error) { block([NSString stringWithFormat:@"Remove error: %@", error]); return; } - + block(nil); } else { block(@"Not support the type"); @@ -1461,7 +1367,7 @@ - (BOOL)favoriteWithId:(NSString *)id favorite:(BOOL)favorite { NSLog(@"Favoriting asset %@ failed: Asset not found.",id); return NO; } - + NSError *error; BOOL canPerformEditOperation = [asset canPerformEditOperation:PHAssetEditOperationProperties]; if (!canPerformEditOperation) { @@ -1495,7 +1401,7 @@ - (void)clearFileCache { NSString *imagePath = [self getCachePath:PM_IMAGE_CACHE_PATH]; NSString *videoPath = [self getCachePath:PM_VIDEO_CACHE_PATH]; NSString *fullFilePath = [self getCachePath:PM_FULL_IMAGE_CACHE_PATH]; - + NSError *err; [PMFileHelper deleteFile:imagePath isDirectory:YES error:err]; if (err) { @@ -1507,13 +1413,13 @@ - (void)clearFileCache { [PMLogUtils.sharedInstance info:[NSString stringWithFormat:@"Remove .video cache %@, error: %@", videoPath, err]]; } - + [PMFileHelper deleteFile:fullFilePath isDirectory:YES error:err]; if (err) { [PMLogUtils.sharedInstance info:[NSString stringWithFormat:@"Remove .full file cache %@, error: %@", fullFilePath, err]]; } - + } #pragma mark cache thumb @@ -1521,15 +1427,15 @@ - (void)clearFileCache { - (void)requestCacheAssetsThumb:(NSArray *)identifiers option:(PMThumbLoadOption *)option { PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:identifiers options:nil]; NSMutableArray *array = [NSMutableArray new]; - + for (id asset in fetchResult) { [array addObject:asset]; } - + PHImageRequestOptions *options = [PHImageRequestOptions new]; options.resizeMode = options.resizeMode; options.deliveryMode = option.deliveryMode; - + [self.cachingManager startCachingImagesForAssets:array targetSize:[option makeSize] contentMode:option.contentMode options:options]; } @@ -1541,7 +1447,7 @@ - (void)notifyProgress:(NSObject *)handler progress: if (!handler) { return; } - + [handler notify:progress state:state]; } @@ -1558,11 +1464,11 @@ - (void)injectModifyToDate:(PMAssetPathEntity *)path { PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[pathId] options:nil]; if (fetchResult.count > 0) { PHAssetCollection *collection = fetchResult.firstObject; - + PHFetchOptions *options = [PHFetchOptions new]; NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"modificationDate" ascending:NO]; options.sortDescriptors = @[sortDescriptor]; - + PHFetchResult *assets = [PHAsset fetchAssetsInAssetCollection:collection options:options]; PHAsset *asset = assets.firstObject; path.modifiedDate = (long) asset.modificationDate.timeIntervalSince1970; diff --git a/lib/photo_manager.dart b/lib/photo_manager.dart index 56cb05d2..e10cdc47 100644 --- a/lib/photo_manager.dart +++ b/lib/photo_manager.dart @@ -7,8 +7,14 @@ /// To use, import `package:photo_manager/photo_manager.dart`. library photo_manager; -export 'src/filter/filter_option_group.dart'; -export 'src/filter/filter_options.dart'; +export 'src/filter/base_filter.dart'; +export 'src/filter/classical/filter_option_group.dart'; +export 'src/filter/classical/filter_options.dart'; + +export 'src/filter/custom/custom_filter.dart'; +export 'src/filter/custom/advance.dart'; +export 'src/filter/custom/custom_columns.dart'; +export 'src/filter/custom/order_by_item.dart'; export 'src/internal/enums.dart'; export 'src/internal/extensions.dart'; @@ -23,3 +29,5 @@ export 'src/managers/photo_manager.dart'; export 'src/types/entity.dart'; export 'src/types/thumbnail.dart'; export 'src/types/types.dart'; + +export 'src/utils/column_utils.dart'; diff --git a/lib/src/filter/base_filter.dart b/lib/src/filter/base_filter.dart new file mode 100644 index 00000000..deeaea1e --- /dev/null +++ b/lib/src/filter/base_filter.dart @@ -0,0 +1,84 @@ +import 'custom/custom_columns.dart'; +import 'custom/custom_filter.dart'; +import 'custom/order_by_item.dart'; + +/// The type of the filter. +enum BaseFilterType { + /// The classical filter. + classical, + + /// The custom filter. + custom, +} + +/// The extension of [BaseFilterType]. +extension BaseFilterTypeExtension on BaseFilterType { + /// The value of the [BaseFilterType]. + int get value { + switch (this) { + case BaseFilterType.classical: + return 0; + case BaseFilterType.custom: + return 1; + } + } +} + +/// The base class of all the filters. +/// +/// See also: +abstract class PMFilter { + /// Construct a default filter. + PMFilter(); + + /// Construct a default filter. + factory PMFilter.defaultValue({ + bool containsPathModified = false, + }) { + return CustomFilter.sql( + where: '', + orderBy: [ + OrderByItem.named( + column: CustomColumns.base.createDate, + isAsc: false, + ), + ], + ); + } + + /// Whether the [AssetPathEntity]s will return with modified time. + /// + /// This option is performance-consuming. Use with cautious. + /// + /// See also: + /// * [AssetPathEntity.lastModified]. + bool containsPathModified = false; + + /// The type of the filter. + BaseFilterType get type; + + /// The child map of the filter. + /// + /// The subclass should override this method to make params. + Map childMap(); + + /// The method only support for [FilterOptionGroup]. + PMFilter updateDateToNow(); + + /// Convert the filter to a map for channel. + Map toMap() { + return { + 'type': type.value, + 'child': { + ...childMap(), + ..._paramMap(), + }, + }; + } + + Map _paramMap() { + return { + 'containsPathModified': containsPathModified, + }; + } +} diff --git a/lib/src/filter/filter_option_group.dart b/lib/src/filter/classical/filter_option_group.dart similarity index 88% rename from lib/src/filter/filter_option_group.dart rename to lib/src/filter/classical/filter_option_group.dart index e5f52e23..774e8c81 100644 --- a/lib/src/filter/filter_option_group.dart +++ b/lib/src/filter/classical/filter_option_group.dart @@ -4,23 +4,25 @@ import 'dart:convert'; -import '../internal/enums.dart'; +import '../../internal/enums.dart'; +import '../base_filter.dart'; import 'filter_options.dart'; /// The group class to obtain [FilterOption]s. -class FilterOptionGroup { +class FilterOptionGroup extends PMFilter { /// Construct a default options group. FilterOptionGroup({ FilterOption imageOption = const FilterOption(), FilterOption videoOption = const FilterOption(), FilterOption audioOption = const FilterOption(), - this.containsPathModified = false, + bool containsPathModified = false, this.containsLivePhotos = true, this.onlyLivePhotos = false, DateTimeCond? createTimeCond, DateTimeCond? updateTimeCond, List orders = const [], }) { + super.containsPathModified = containsPathModified; _map[AssetType.image] = imageOption; _map[AssetType.video] = videoOption; _map[AssetType.audio] = audioOption; @@ -32,6 +34,16 @@ class FilterOptionGroup { /// Construct an empty options group. FilterOptionGroup.empty(); + /// Whether to obtain only live photos. + /// + /// This option only takes effects on iOS and when the request type is image. + bool onlyLivePhotos = false; + + /// Whether to obtain live photos. + /// + /// This option only takes effects on iOS. + bool containsLivePhotos = true; + final Map _map = {}; /// Get the [FilterOption] according the specific [AssetType]. @@ -42,24 +54,6 @@ class FilterOptionGroup { _map[type] = option; } - /// Whether the [AssetPathEntity]s will return with modified time. - /// - /// This option is performance-consuming. Use with cautious. - /// - /// See also: - /// * [AssetPathEntity.lastModified]. - bool containsPathModified = false; - - /// Whether to obtain live photos. - /// - /// This option only takes effects on iOS. - bool containsLivePhotos = true; - - /// Whether to obtain only live photos. - /// - /// This option only takes effects on iOS and when the request type is image. - bool onlyLivePhotos = false; - DateTimeCond createTimeCond = DateTimeCond.def(); DateTimeCond updateTimeCond = DateTimeCond.def().copyWith(ignore: true); @@ -83,7 +77,20 @@ class FilterOptionGroup { ..addAll(other.orders); } - Map toMap() { + @override + FilterOptionGroup updateDateToNow() { + return copyWith( + createTimeCond: createTimeCond.copyWith( + max: DateTime.now(), + ), + updateTimeCond: updateTimeCond.copyWith( + max: DateTime.now(), + ), + ); + } + + @override + Map childMap() { return { if (_map.containsKey(AssetType.image)) 'image': getOption(AssetType.image).toMap(), @@ -91,12 +98,11 @@ class FilterOptionGroup { 'video': getOption(AssetType.video).toMap(), if (_map.containsKey(AssetType.audio)) 'audio': getOption(AssetType.audio).toMap(), - 'containsPathModified': containsPathModified, - 'containsLivePhotos': containsLivePhotos, - 'onlyLivePhotos': onlyLivePhotos, 'createDate': createTimeCond.toMap(), 'updateDate': updateTimeCond.toMap(), 'orders': orders.map((OrderOption e) => e.toMap()).toList(), + 'containsLivePhotos': containsLivePhotos, + 'onlyLivePhotos': onlyLivePhotos, }; } @@ -139,4 +145,7 @@ class FilterOptionGroup { String toString() { return const JsonEncoder.withIndent(' ').convert(toMap()); } + + @override + BaseFilterType get type => BaseFilterType.classical; } diff --git a/lib/src/filter/filter_options.dart b/lib/src/filter/classical/filter_options.dart similarity index 99% rename from lib/src/filter/filter_options.dart rename to lib/src/filter/classical/filter_options.dart index 5004c38b..b9da5783 100644 --- a/lib/src/filter/filter_options.dart +++ b/lib/src/filter/classical/filter_options.dart @@ -6,7 +6,7 @@ import 'dart:convert'; import 'package:flutter/widgets.dart'; -import '../internal/enums.dart'; +import '../../internal/enums.dart'; /// A series of filter options for [AssetType] when querying assets. @immutable diff --git a/lib/src/filter/custom/advance.dart b/lib/src/filter/custom/advance.dart new file mode 100644 index 00000000..2f28bcc1 --- /dev/null +++ b/lib/src/filter/custom/advance.dart @@ -0,0 +1,443 @@ +import 'dart:io'; + +import 'custom_columns.dart'; +import 'custom_filter.dart'; +import 'order_by_item.dart'; + +/// {@template PM.AdvancedCustomFilter} +/// +/// The advanced custom filter. +/// +/// The [AdvancedCustomFilter] is a more powerful helper. +/// +/// Examples: +/// ```dart +/// final filter = AdvancedCustomFilter() +/// .addWhereCondition( +/// ColumnWhereCondition( +/// column: _columns.width, +/// operator: '>=', +/// value: '200', +/// ), +/// ) +/// .addOrderBy(column: _columns.createDate, isAsc: false); +/// ``` +/// +/// {@endtemplate} +class AdvancedCustomFilter extends CustomFilter { + final List _whereItemList = []; + final List _orderByItemList = []; + + /// {@macro PM.AdvancedCustomFilter} + AdvancedCustomFilter({ + List where = const [], + List orderBy = const [], + }) { + _whereItemList.addAll(where); + _orderByItemList.addAll(orderBy); + } + + /// Add a [WhereConditionItem] to the filter. + AdvancedCustomFilter addWhereCondition( + WhereConditionItem condition, { + LogicalType type = LogicalType.and, + }) { + condition.logicalType = type; + _whereItemList.add(condition); + return this; + } + + /// Add a [OrderByItem] to the filter. + AdvancedCustomFilter addOrderBy({ + required String column, + bool isAsc = true, + }) { + _orderByItemList.add(OrderByItem(column, isAsc)); + return this; + } + + @override + String makeWhere() { + final sb = StringBuffer(); + for (final item in _whereItemList) { + if (sb.isNotEmpty) { + sb.write(' ${item.logicalType == LogicalType.and ? 'AND' : 'OR'} '); + } + sb.write(item.text); + } + return sb.toString(); + } + + @override + List makeOrderBy() { + return _orderByItemList; + } +} + +/// The logical operator used in the [CustomFilter]. +enum LogicalType { + and, + or, +} + +extension LogicalTypeExtension on LogicalType { + int get value { + switch (this) { + case LogicalType.and: + return 0; + case LogicalType.or: + return 1; + } + } +} + +/// {@template PM.column_where_condition} +abstract class WhereConditionItem { + /// The text of the condition. + String get text; + + /// The logical operator used in the [CustomFilter]. + /// + /// See also: + /// - [LogicalType] + LogicalType logicalType = LogicalType.and; + + /// The default constructor. + WhereConditionItem({this.logicalType = LogicalType.and}); + + /// Create a [WhereConditionItem] from a text. + factory WhereConditionItem.text( + String text, { + LogicalType type = LogicalType.and, + }) { + return TextWhereCondition(text, type: type); + } + + /// The platform values. + /// + /// The darwin is different from the android. + /// + /// + static final platformConditions = _platformValues(); + + static List _platformValues() { + if (Platform.isAndroid) { + return [ + 'is not null', + 'is null', + '==', + '!=', + '>', + '>=', + '<', + '<=', + 'like', + 'not like', + 'in', + 'not in', + 'between', + 'not between', + ]; + } else if (Platform.isIOS || Platform.isMacOS) { + // The NSPredicate syntax is used on iOS and macOS. + return [ + '!= nil', + '== nil', + '==', + '!=', + '>', + '>=', + '<', + '<=', + 'like', + 'not like', + 'in', + 'not in', + 'between', + 'not between', + ]; + } + throw UnsupportedError('Unsupported platform'); + } + + /// Same [text] is converted, no readable. + /// + /// So, the method result is used for UI to display. + String display() { + return text; + } +} + +/// {@template PM.column_where_condition_group} +/// +/// The group of [WhereConditionItem] and [WhereConditionGroup]. +/// +/// If you need like `( width > 1000 AND height > 1000) OR ( width < 500 AND height < 500)`, +/// you can use this class to do it. +/// +/// The first item logical type will be ignored. +/// +/// ```dart +/// final filter = AdvancedCustomFilter().addWhereCondition( +/// WhereConditionGroup() +/// .andGroup( +/// WhereConditionGroup().andText('width > 1000').andText('height > 1000'), +/// ) +/// .orGroup( +/// WhereConditionGroup().andText('width < 500').andText('height < 500'), +/// ), +/// ); +/// ``` +/// +/// +/// {@endtemplate} +class WhereConditionGroup extends WhereConditionItem { + final List items = []; + + /// {@macro PM.column_where_condition_group} + WhereConditionGroup(); + + /// Add a [WhereConditionItem] to the group. + /// + /// The logical type is [LogicalType.or]. + WhereConditionGroup and(WhereConditionItem item) { + item.logicalType = LogicalType.and; + items.add(item); + return this; + } + + /// Add a [WhereConditionItem] to the group. + /// + /// The logical type is [LogicalType.or]. + WhereConditionGroup or(WhereConditionItem item) { + item.logicalType = LogicalType.or; + items.add(item); + return this; + } + + /// Add a [text] condition to the group. + /// + /// The logical type is [LogicalType.and]. + WhereConditionGroup andText(String text) { + final item = WhereConditionItem.text(text); + item.logicalType = LogicalType.and; + items.add(item); + return this; + } + + /// Add a [text] condition to the group. + /// + /// The logical type is [LogicalType.or]. + WhereConditionGroup orText(String text) { + final item = WhereConditionItem.text(text); + item.logicalType = LogicalType.or; + items.add(item); + return this; + } + + /// Add a [WhereConditionItem] to the group. + /// + /// The logical type is [LogicalType.and]. + /// + /// See also: + WhereConditionGroup andGroup(WhereConditionGroup group) { + group.logicalType = LogicalType.and; + items.add(group); + return this; + } + + WhereConditionGroup orGroup(WhereConditionGroup group) { + group.logicalType = LogicalType.or; + items.add(group); + return this; + } + + @override + String get text { + if (items.isEmpty) { + return ''; + } + + final sb = StringBuffer(); + for (final item in items) { + final text = item.text; + if (text.isEmpty) { + continue; + } + if (sb.isNotEmpty) { + sb.write(' ${item.logicalType == LogicalType.and ? 'AND' : 'OR'} '); + } + sb.write(item.text); + } + + return '( $sb )'; + } + + @override + String display() { + final sb = StringBuffer(); + for (final item in items) { + if (sb.isNotEmpty) { + sb.write(' ${item.logicalType == LogicalType.and ? 'AND' : 'OR'} '); + } + sb.write(item.display()); + } + + return '( $sb )'; + } +} + +bool _checkDateColumn(String column) { + return CustomColumns.dateColumns().contains(column); +} + +bool _checkOtherColumn(String column) { + if (Platform.isAndroid) { + const android = CustomColumns.android; + return android.getValues().contains(column); + } else if (Platform.isIOS || Platform.isMacOS) { + const darwin = CustomColumns.darwin; + return darwin.getValues().contains(column); + } + return false; +} + +/// {@template PM.column_where_condition} +/// +/// The where condition item. +/// +/// The [operator] is the operator of the condition. +/// +/// The [value] is the value of the condition. +/// +/// {@endtemplate} +class ColumnWhereCondition extends WhereConditionItem { + /// - Android: the column name in the MediaStore database. + /// - iOS/macOS: the field with the PHAsset. + final String column; + + /// such as: `=`, `>`, `>=`, `!=`, `like`, `in`, `between`, `is null`, `is not null`. + final String? operator; + + /// The value of the condition. + final String? value; + + /// Check the column when the [text] is called. Default is true. + /// + /// If false, don't check the column. + final bool needCheck; + + /// {@macro PM.column_where_condition} + ColumnWhereCondition({ + required this.column, + required this.operator, + required this.value, + this.needCheck = true, + }) : super(); + + @override + String get text { + if (needCheck && _checkDateColumn(column)) { + assert(needCheck && _checkDateColumn(column), + 'The column: $column is date type, please use DateColumnWhereCondition'); + + return ''; + } + + if (needCheck && !_checkOtherColumn(column)) { + assert(needCheck && !_checkOtherColumn(column), + 'The $column is not support the platform, please check.'); + return ''; + } + + final sb = StringBuffer(); + sb.write(column); + if (operator != null) { + sb.write(' ${operator!} '); + } + if (value != null) { + sb.write(value!); + } + return sb.toString(); + } +} + +/// {@template PM.date_column_where_condition} +/// +/// The where condition item for date type. +/// +/// Because the date type is different between Android and iOS/macOS. +/// +/// {@endtemplate} +class DateColumnWhereCondition extends WhereConditionItem { + /// The column name of the date type. + final String column; + + /// such as: `=`, `>`, `>=`, `!=`, `like`, `in`, `between`, `is null`, `is not null`. + final String operator; + + /// The value of the condition. + final DateTime value; + final bool checkColumn; + + DateColumnWhereCondition({ + required this.column, + required this.operator, + required this.value, + this.checkColumn = true, + }) : super(); + + @override + String get text { + if (checkColumn && !_checkDateColumn(column)) { + assert(checkColumn && !_checkDateColumn(column), + 'The date column just support createDate, modifiedDate, dateTaken, dateExpires'); + return ''; + } + final sb = StringBuffer(); + sb.write(column); + sb.write(' $operator '); + var isSecond = true; + if (Platform.isAndroid) { + isSecond = column != CustomColumns.android.dateTaken; + } + final sql = + CustomColumns.utils.convertDateTimeToSql(value, isSeconds: isSecond); + sb.write(' $sql'); + return sb.toString(); + } + + @override + String display() { + final sb = StringBuffer(); + sb.write(column); + sb.write(' $operator '); + sb.write(' ${value.toIso8601String()}'); + return sb.toString(); + } +} + +/// {@template PM.text_where_condition} +/// +/// The where condition item for text. +/// +/// It is recommended to use +/// [DateColumnWhereCondition] or [ColumnWhereCondition] instead of this one, +/// because different platforms may have different syntax. +/// +/// If you are an advanced user and insist on using it, +/// please understand the following: +/// - Android: How to write where with `ContentReslover`. +/// - iOS/macOS: How to format `NSPredicate`. +/// +/// {@endtemplate} +class TextWhereCondition extends WhereConditionItem { + @override + final String text; + + /// {@macro PM.text_where_condition} + TextWhereCondition( + this.text, { + LogicalType type = LogicalType.and, + }) : super(logicalType: type); +} diff --git a/lib/src/filter/custom/custom_columns.dart b/lib/src/filter/custom/custom_columns.dart new file mode 100644 index 00000000..6aaa32ee --- /dev/null +++ b/lib/src/filter/custom/custom_columns.dart @@ -0,0 +1,455 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +import '../../utils/column_utils.dart'; + +/// {@template custom_columns} +/// +/// A class that contains the names of the columns used in the custom filter. +/// +/// The names of the columns are different on different platforms. +/// +/// For example, the `width` column on Android is `width`, but on iOS it is `pixelWidth`. +/// +/// The definition of the column name can be found in next link: +/// +/// Android: https://developer.android.com/reference/android/provider/MediaStore.MediaColumns +/// +/// iOS: https://developer.apple.com/documentation/photokit/phasset +/// +/// Special Columns to see [AndroidMediaColumns] and [DarwinColumns]. +/// +/// Example: +/// ```dart +/// OrderByItem(CustomColumns.base.width, true); +/// ``` +/// +/// {@endtemplate} +class CustomColumns { + /// The base columns, contains the common columns. + static const CustomColumns base = CustomColumns(); + + /// The android columns, contains the android specific columns. + static const AndroidMediaColumns android = AndroidMediaColumns(); + + /// The darwin columns, contains the ios and macos specific columns. + static const DarwinColumns darwin = DarwinColumns(); + + static const ColumnUtils utils = ColumnUtils.instance; + + /// {@macro custom_columns} + const CustomColumns(); + + /// Whether the current platform is android. + bool get isAndroid => Platform.isAndroid; + + /// Whether the current platform is ios or macos. + bool get isDarwin => Platform.isIOS || Platform.isMacOS; + + /// The id column. + String get id { + if (isAndroid) { + return '_id'; + } else if (isDarwin) { + return 'localIdentifier'; + } else { + throw UnsupportedError('Unsupported platform with id'); + } + } + + /// The media type column. + /// + /// The value is number, + /// + /// In android: + /// - 1: image + /// - 2: audio + /// - 3: video + /// + /// In iOS/macOS: + /// - 1: image + /// - 2: video + /// - 3: audio + String get mediaType { + if (isAndroid) { + return 'media_type'; + } else if (isDarwin) { + return 'mediaType'; + } else { + throw UnsupportedError('Unsupported platform with mediaType'); + } + } + + /// The width column. + /// + /// In android, the value of this column maybe null. + /// + /// In iOS/macOS, the value of this column not null. + String get width { + if (isAndroid) { + return 'width'; + } else if (isDarwin) { + return 'pixelWidth'; + } else { + throw UnsupportedError('Unsupported platform with width'); + } + } + + /// The height column. + /// + /// In android, the value of this column maybe null. + /// + /// In iOS/macOS, the value of this column not null. + String get height { + if (isAndroid) { + return 'height'; + } else if (isDarwin) { + return 'pixelHeight'; + } else { + throw UnsupportedError('Unsupported platform with height'); + } + } + + /// The duration column. + /// + /// In android, the value of this column maybe null. + /// + /// In iOS/macOS, the value of this column is 0 when the media is image. + String get duration { + if (isAndroid) { + return 'duration'; + } else if (isDarwin) { + return 'duration'; + } else { + throw UnsupportedError('Unsupported platform with duration'); + } + } + + /// The creation date column. + /// + /// {@template date_column} + /// + /// The value is unix timestamp seconds in android. + /// + /// The value is NSDate in iOS/macOS. + /// + /// Please use [ColumnUtils.convertDateTimeToSql] to convert date value. + /// + /// Simple use: [DateColumnWhereCondition]. + /// + /// Exmaple: + /// ```dart + /// final date = DateTime(2015, 6, 15); + /// final condition = DateColumnWhereCondition( + // column: CustomColumns.base.createDate, + // operator: '<=', + // value: date, + // ); + /// ``` + /// + /// {@endtemplate} + String get createDate { + if (isAndroid) { + return 'date_added'; + } else if (isDarwin) { + return 'creationDate'; + } else { + throw UnsupportedError('Unsupported platform with createDate'); + } + } + + /// The modified date column. + /// + /// {@macro date_column} + String get modifiedDate { + if (isAndroid) { + return 'date_modified'; + } else if (isDarwin) { + return 'modificationDate'; + } else { + throw UnsupportedError('Unsupported platform with modifiedDate'); + } + } + + /// The favorite column. + /// + /// in darwin: 1 is favorite, 0 is not favorite. + /// + /// + String get isFavorite { + if (isAndroid) { + return 'is_favorite'; + } else if (isDarwin) { + return 'favorite'; + } else { + throw UnsupportedError('Unsupported platform with isFavorite'); + } + } + + List getValues() { + return [ + id, + mediaType, + width, + height, + duration, + createDate, + modifiedDate, + isFavorite, + ]; + } + + static List values() { + return const CustomColumns().getValues(); + } + + static List dateColumns() { + if (Platform.isAndroid) { + const android = CustomColumns.android; + return [ + android.createDate, + android.modifiedDate, + android.dateTaken, + android.dateExpires, + ]; + } else if (Platform.isIOS || Platform.isMacOS) { + const darwin = CustomColumns.darwin; + return [darwin.createDate, darwin.modifiedDate]; + } + return []; + } + + static List platformValues() { + if (Platform.isAndroid) { + return const AndroidMediaColumns().getValues(); + } else if (Platform.isIOS || Platform.isMacOS) { + return const DarwinColumns().getValues(); + } else { + throw UnsupportedError('Unsupported platform with platformValues'); + } + } +} + +// columns: [instance_id, compilation, disc_number, duration, album_artist, +// resolution, orientation, artist, author, format, height, is_drm, +// bucket_display_name, owner_package_name, parent, volume_name, +// date_modified, writer, date_expires, composer, +// _display_name, datetaken, mime_type, bitrate, cd_track_number, _id, +// xmp, year, _data, _size, album, genre, title, width, is_favorite, +// is_trashed, group_id, document_id, generation_added, is_download, +// generation_modified, is_pending, date_added, capture_framerate, num_tracks, +// original_document_id, bucket_id, media_type, relative_path] + +/// A class that contains the names of the columns used in the custom filter. +/// +/// About the values mean, please see document of android: https://developer.android.com/reference/android/provider/MediaStore +class AndroidMediaColumns extends CustomColumns { + const AndroidMediaColumns(); + + String _getKey(String value) { + if (isAndroid) { + return value; + } else { + throw UnsupportedError('Unsupported column $value in platform'); + } + } + + String get instanceId => _getKey('instance_id'); + + String get compilation => _getKey('compilation'); + + String get discNumber => _getKey('disc_number'); + + String get albumArtist => _getKey('album_artist'); + + String get resolution => _getKey('resolution'); + + String get orientation => _getKey('orientation'); + + String get artist => _getKey('artist'); + + String get author => _getKey('author'); + + String get format => _getKey('format'); + + String get isDrm => _getKey('is_drm'); + + String get bucketDisplayName => _getKey('bucket_display_name'); + + String get ownerPackageName => _getKey('owner_package_name'); + + String get parent => _getKey('parent'); + + String get volumeName => _getKey('volume_name'); + + String get writer => _getKey('writer'); + + String get dateExpires => _getKey('date_expires'); + + String get composer => _getKey('composer'); + + String get displayName => _getKey('_display_name'); + + String get dateTaken => _getKey('datetaken'); + + String get mimeType => _getKey('mime_type'); + + String get bitRate => _getKey('bitrate'); + + String get cdTrackNumber => _getKey('cd_track_number'); + + String get xmp => _getKey('xmp'); + + String get year => _getKey('year'); + + String get data => _getKey('_data'); + + String get size => _getKey('_size'); + + String get album => _getKey('album'); + + String get genre => _getKey('genre'); + + String get title => _getKey('title'); + + String get isTrashed => _getKey('is_trashed'); + + String get groupId => _getKey('group_id'); + + String get documentId => _getKey('document_id'); + + String get generationAdded => _getKey('generation_added'); + + String get isDownload => _getKey('is_download'); + + String get generationModified => _getKey('generation_modified'); + + String get isPending => _getKey('is_pending'); + + String get captureFrameRate => _getKey('capture_framerate'); + + String get numTracks => _getKey('num_tracks'); + + String get originalDocumentId => _getKey('original_document_id'); + + String get bucketId => _getKey('bucket_id'); + + String get relativePath => _getKey('relative_path'); + + @override + List getValues() { + return [ + ...super.getValues(), + instanceId, + compilation, + discNumber, + albumArtist, + resolution, + orientation, + artist, + author, + format, + isDrm, + bucketDisplayName, + ownerPackageName, + parent, + volumeName, + writer, + dateExpires, + composer, + displayName, + dateTaken, + mimeType, + bitRate, + cdTrackNumber, + xmp, + year, + data, + size, + album, + genre, + title, + isTrashed, + groupId, + documentId, + generationAdded, + isDownload, + generationModified, + isPending, + captureFrameRate, + numTracks, + originalDocumentId, + bucketId, + relativePath, + ]; + } + + static List values() { + return const AndroidMediaColumns().getValues(); + } +} + +/// A class that contains the names of the columns of the iOS/macOS platform. +/// +/// About the values mean, please see document of iOS: https://developer.apple.com/documentation/photokit/phasset +class DarwinColumns extends CustomColumns { + const DarwinColumns(); + + String _getKey(String value) { + if (isDarwin) { + return value; + } else { + throw UnsupportedError('Unsupported column $value in platform'); + } + } + + String get mediaSubtypes => _getKey('mediaSubtypes'); + + String get sourceType => _getKey('sourceType'); + + String get location => _getKey('location'); + + String get hidden => _getKey('hidden'); + + String get hasAdjustments => _getKey('hasAdjustments'); + + String get adjustmentFormatIdentifier => + _getKey('adjustmentFormatIdentifier'); + + @override + List getValues() { + return [ + ...super.getValues(), + mediaSubtypes, + sourceType, + location, + hidden, + hasAdjustments, + adjustmentFormatIdentifier, + ]; + } + + static List values() { + return const DarwinColumns().getValues(); + } +} + +/// Where item class +/// +/// If text() throws an exception, it will return an empty string. +class WhereItem { + final ValueGetter column; + + final ValueGetter condition; + + WhereItem(this.column, this.condition); + + String text() { + try { + return "${column()} ${condition()}"; + } on UnsupportedError { + return ''; + } + } +} diff --git a/lib/src/filter/custom/custom_filter.dart b/lib/src/filter/custom/custom_filter.dart new file mode 100644 index 00000000..ce4dc890 --- /dev/null +++ b/lib/src/filter/custom/custom_filter.dart @@ -0,0 +1,92 @@ +import '../base_filter.dart'; +import 'order_by_item.dart'; + +/// Full custom filter. +/// +/// Use the filter to filter all the assets. +/// +/// Actually, it is a sql filter. +/// In android: convert where and orderBy to the params of ContentResolver.query +/// In iOS/macOS: convert where and orderBy to the PHFetchOptions to filter the assets. +/// +/// Now, the [CustomFilter] is have two sub class: +/// [CustomFilter.sql] to create [SqlCustomFilter]. +/// +/// The [AdvancedCustomFilter] is a more powerful helper. +/// +/// Examples: +/// {@macro PM.sql_custom_filter} +/// +/// See also: +/// - [CustomFilter.sql] +/// - [AdvancedCustomFilter] +/// - [OrderByItem] +abstract class CustomFilter extends PMFilter { + CustomFilter(); + + factory CustomFilter.sql({ + required String where, + List orderBy = const [], + }) { + return SqlCustomFilter(where, orderBy); + } + + @override + BaseFilterType get type => BaseFilterType.custom; + + @override + Map childMap() { + return { + 'where': makeWhere(), + 'orderBy': makeOrderBy().map((e) => e.toMap()).toList(), + }; + } + + @override + PMFilter updateDateToNow() { + return this; + } + + /// Make the where condition. + String makeWhere(); + + /// Make the order by condition. + List makeOrderBy(); +} + +/// {@template PM.sql_custom_filter} +/// +/// The sql custom filter. +/// +/// create example: +/// +/// ```dart +/// final filter = CustomFilter.sql( +/// where: '${CustomColumns.base.width} > 1000', +/// orderBy: [ +/// OrderByItem(CustomColumns.base.width, desc), +/// ], +/// ); +/// ``` +/// +/// {@endtemplate} +class SqlCustomFilter extends CustomFilter { + /// The where condition. + final String where; + + /// The order by condition. + final List orderBy; + + /// {@macro PM.sql_custom_filter} + SqlCustomFilter(this.where, this.orderBy); + + @override + String makeWhere() { + return where; + } + + @override + List makeOrderBy() { + return orderBy; + } +} diff --git a/lib/src/filter/custom/order_by_item.dart b/lib/src/filter/custom/order_by_item.dart new file mode 100644 index 00000000..50c6ccec --- /dev/null +++ b/lib/src/filter/custom/order_by_item.dart @@ -0,0 +1,52 @@ +/// {@template PM.order_by_item} +/// +/// The order by item. +/// +/// Example: +/// ```dart +/// OrderByItem(CustomColumns.base.width, true); +/// ``` +/// +/// See also: +/// - [CustomFilter] +/// - [CustomColumns.base] +/// - [CustomColumns.android] +/// - [CustomColumns.darwin] +/// - [CustomColumns.platformValues] +/// +/// {@endtemplate} +class OrderByItem { + /// The column name. + final String column; + + /// The order type. + final bool isAsc; + + /// {@macro PM.order_by_item} + const OrderByItem(this.column, this.isAsc); + + /// {@macro PM.order_by_item} + const OrderByItem.desc(this.column) : isAsc = false; + + /// {@macro PM.order_by_item} + const OrderByItem.asc(this.column) : isAsc = true; + + /// {@macro PM.order_by_item} + const OrderByItem.named({ + required this.column, + this.isAsc = true, + }); + + /// Convert to the map. + Map toMap() { + return { + 'column': column, + 'isAsc': isAsc, + }; + } + + @override + String toString() { + return 'OrderByItem{column: $column, isAsc: $isAsc}'; + } +} diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index b7ff716d..53964b1f 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -56,6 +56,10 @@ class PMConstants { static const String mCreateFolder = 'createFolder'; static const String mRemoveInAlbum = 'removeInAlbum'; static const String mMoveAssetToPath = 'moveAssetToPath'; + static const String mColumnNames = 'getColumnNames'; + + static const String mGetAssetCount = 'getAssetCount'; + static const String mGetAssetsByRange = 'getAssetsByRange'; /// Constant value. static const int vDefaultThumbnailSize = 150; diff --git a/lib/src/internal/plugin.dart b/lib/src/internal/plugin.dart index 50ca6de9..b476e1b3 100644 --- a/lib/src/internal/plugin.dart +++ b/lib/src/internal/plugin.dart @@ -8,7 +8,8 @@ import 'dart:typed_data' as typed_data; import 'package:flutter/services.dart'; -import '../filter/filter_option_group.dart'; +import '../filter/base_filter.dart'; +import '../filter/classical/filter_option_group.dart'; import '../types/entity.dart'; import '../types/thumbnail.dart'; import '../types/types.dart'; @@ -29,7 +30,7 @@ class PhotoManagerPlugin with BasePlugin, IosPlugin, AndroidPlugin { bool hasAll = true, bool onlyAll = false, RequestType type = RequestType.common, - FilterOptionGroup? filterOption, + PMFilter? filterOption, }) async { if (onlyAll) { assert(hasAll, 'If only is true, then the hasAll must be not null.'); @@ -37,16 +38,19 @@ class PhotoManagerPlugin with BasePlugin, IosPlugin, AndroidPlugin { filterOption ??= FilterOptionGroup(); // Avoid filtering live photos when searching for audios. if (type == RequestType.audio) { - filterOption = filterOption.copyWith( - containsLivePhotos: false, - onlyLivePhotos: false, + if (filterOption is FilterOptionGroup) { + filterOption.containsLivePhotos = false; + filterOption.onlyLivePhotos = false; + } + } + if (filterOption is FilterOptionGroup) { + assert( + type == RequestType.image || !filterOption.onlyLivePhotos, + 'Filtering only Live Photos is only supported ' + 'when the request type contains image.', ); } - assert( - type == RequestType.image || !filterOption.onlyLivePhotos, - 'Filtering only Live Photos is only supported ' - 'when the request type contains image.', - ); + final Map? result = await _channel.invokeMethod( PMConstants.mGetAssetPathList, { @@ -97,7 +101,7 @@ class PhotoManagerPlugin with BasePlugin, IosPlugin, AndroidPlugin { /// Use pagination to get album content. Future> getAssetListPaged( String id, { - required FilterOptionGroup optionGroup, + required PMFilter optionGroup, int page = 0, int size = 15, RequestType type = RequestType.common, @@ -122,7 +126,7 @@ class PhotoManagerPlugin with BasePlugin, IosPlugin, AndroidPlugin { required RequestType type, required int start, required int end, - required FilterOptionGroup optionGroup, + required PMFilter optionGroup, }) async { final Map map = await _channel.invokeMethod>( @@ -208,7 +212,7 @@ class PhotoManagerPlugin with BasePlugin, IosPlugin, AndroidPlugin { Future?> fetchPathProperties( String id, RequestType type, - FilterOptionGroup optionGroup, + PMFilter optionGroup, ) { return _channel.invokeMethod( PMConstants.mFetchPathProperties, @@ -546,6 +550,38 @@ class PhotoManagerPlugin with BasePlugin, IosPlugin, AndroidPlugin { } return null; } + + Future getAssetCount({ + PMFilter? filterOption, + RequestType type = RequestType.common, + }) { + final filter = filterOption ?? PMFilter.defaultValue(); + + return _channel.invokeMethod(PMConstants.mGetAssetCount, { + 'type': type.value, + 'option': filter.toMap(), + }).then((v) => v ?? 0); + } + + Future> getAssetListWithRange({ + required int start, + required int end, + RequestType type = RequestType.common, + PMFilter? filterOption, + }) { + final filter = filterOption ?? PMFilter.defaultValue(); + return _channel.invokeMethod(PMConstants.mGetAssetsByRange, { + 'type': type.value, + 'start': start, + 'end': end, + 'option': filter.toMap(), + }).then((value) { + if (value == null) return []; + return ConvertUtils.convertToAssetList( + value.cast(), + ); + }); + } } mixin IosPlugin on BasePlugin { @@ -636,4 +672,14 @@ mixin AndroidPlugin on BasePlugin { ); return result != null; } + + Future> androidColumns() async { + final result = await _channel.invokeMethod( + PMConstants.mColumnNames, + ); + if (result is List) { + return result.map((e) => e.toString()).toList(); + } + return result ?? []; + } } diff --git a/lib/src/managers/photo_manager.dart b/lib/src/managers/photo_manager.dart index 9bb09660..09d5da02 100644 --- a/lib/src/managers/photo_manager.dart +++ b/lib/src/managers/photo_manager.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import '../filter/filter_option_group.dart'; +import '../filter/base_filter.dart'; import '../internal/editor.dart'; import '../internal/enums.dart'; import '../internal/plugin.dart' as base; @@ -70,7 +70,7 @@ class PhotoManager { /// Obtain albums/folders list with couple filter options. /// /// To obtain albums list that contains the root album - /// (generally named "Recent"), set [hasAll] to true. + /// (generally named 'Recent'), set [hasAll] to true. /// /// To obtain only the root album in the list, set [onlyAll] to true. /// @@ -81,7 +81,7 @@ class PhotoManager { bool hasAll = true, bool onlyAll = false, RequestType type = RequestType.common, - FilterOptionGroup? filterOption, + PMFilter? filterOption, }) async { return plugin.getAssetPathList( hasAll: hasAll, @@ -157,4 +157,65 @@ class PhotoManager { /// Clear all file caches. static Future clearFileCache() => plugin.clearFileCache(); + + /// Get the asset count + static Future getAssetCount({ + PMFilter? filterOption, + RequestType type = RequestType.common, + }) { + return plugin.getAssetCount(filterOption: filterOption, type: type); + } + + /// Get the asset list with range. + /// + /// The [start] is base 0. + /// + /// The [end] is not included. + /// + /// The [filterOption] is used to filter the assets. + /// + /// The [type] is used to filter the assets type. + static Future> getAssetListRange({ + required int start, + required int end, + PMFilter? filterOption, + RequestType type = RequestType.common, + }) async { + assert(start >= 0, 'start must >= 0'); + assert(end >= 0, 'end must >= 0'); + assert(start < end, 'start must < end'); + return plugin.getAssetListWithRange( + start: start, + end: end, + filterOption: filterOption, + type: type, + ); + } + + /// Get the asset list with page. + /// + /// The [page] is base 0. + /// + /// The [pageCount] is the count of each page. + /// + /// The [filterOption] is used to filter the assets. + /// + /// The [type] is used to filter the assets type. + static Future> getAssetListPaged({ + required int page, + required int pageCount, + PMFilter? filterOption, + RequestType type = RequestType.common, + }) async { + assert(page >= 0, 'page must >= 0'); + assert(pageCount > 0, 'pageCount must > 0'); + final start = page * pageCount; + final end = start + pageCount; + return getAssetListRange( + start: start, + end: end, + filterOption: filterOption, + type: type, + ); + } } diff --git a/lib/src/types/entity.dart b/lib/src/types/entity.dart index 6f739a5a..00f75573 100644 --- a/lib/src/types/entity.dart +++ b/lib/src/types/entity.dart @@ -8,7 +8,8 @@ import 'dart:typed_data' as typed_data; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; -import '../filter/filter_option_group.dart'; +import '../filter/base_filter.dart'; +import '../filter/classical/filter_option_group.dart'; import '../internal/constants.dart'; import '../internal/editor.dart'; import '../internal/enums.dart'; @@ -32,7 +33,7 @@ class AssetPathEntity { this.lastModified, this.type = RequestType.common, this.isAll = false, - FilterOptionGroup? filterOption, + PMFilter? filterOption, }) : filterOption = filterOption ??= FilterOptionGroup(); /// Obtain an entity from ID. @@ -99,29 +100,32 @@ class AssetPathEntity { final bool isAll; /// The collection of filter options of the album. - final FilterOptionGroup filterOption; + final PMFilter filterOption; /// Call this method to obtain new path entity. static Future obtainPathFromProperties({ required String id, int albumType = 1, RequestType type = RequestType.common, - FilterOptionGroup? optionGroup, + PMFilter? optionGroup, bool maxDateTimeToNow = true, }) async { optionGroup ??= FilterOptionGroup(); final StateError error = StateError( 'Unable to fetch properties for path $id.', ); + if (maxDateTimeToNow) { - optionGroup = optionGroup.copyWith( - createTimeCond: optionGroup.createTimeCond.copyWith( - max: DateTime.now(), - ), - updateTimeCond: optionGroup.updateTimeCond.copyWith( - max: DateTime.now(), - ), - ); + if (optionGroup is FilterOptionGroup) { + optionGroup = optionGroup.copyWith( + createTimeCond: optionGroup.createTimeCond.copyWith( + max: DateTime.now(), + ), + updateTimeCond: optionGroup.updateTimeCond.copyWith( + max: DateTime.now(), + ), + ); + } } else { optionGroup = optionGroup; } @@ -167,11 +171,16 @@ class AssetPathEntity { }) { assert(albumType == 1, 'Only album can request for assets.'); assert(size > 0, 'Page size must be greater than 0.'); - assert( - type == RequestType.image || !filterOption.onlyLivePhotos, - 'Filtering only Live Photos is only supported ' - 'when the request type contains image.', - ); + + final filterOption = this.filterOption; + + if (filterOption is FilterOptionGroup) { + assert( + type == RequestType.image || !filterOption.onlyLivePhotos, + 'Filtering only Live Photos is only supported ' + 'when the request type contains image.', + ); + } return plugin.getAssetListPaged( id, page: page, @@ -193,11 +202,15 @@ class AssetPathEntity { assert(albumType == 1, 'Only album can request for assets.'); assert(start >= 0, 'The start must be greater than 0.'); assert(end > start, 'The end must be greater than start.'); - assert( - type == RequestType.image || !filterOption.onlyLivePhotos, - 'Filtering only Live Photos is only supported ' - 'when the request type contains image.', - ); + final filterOption = this.filterOption; + + if (filterOption is FilterOptionGroup) { + assert( + type == RequestType.image || !filterOption.onlyLivePhotos, + 'Filtering only Live Photos is only supported ' + 'when the request type contains image.', + ); + } final int count = await assetCountAsync; if (end > count) { end = count; diff --git a/lib/src/utils/column_utils.dart b/lib/src/utils/column_utils.dart new file mode 100644 index 00000000..04a3f3fc --- /dev/null +++ b/lib/src/utils/column_utils.dart @@ -0,0 +1,24 @@ +import 'dart:io'; + +/// The utils for the [CustomColumns]. +class ColumnUtils { + const ColumnUtils._internal(); + + static const ColumnUtils instance = ColumnUtils._internal(); + + /// Convert to the format for the [MediaStore] in android or [NSPredicate] in iOS. + String convertDateTimeToSql(DateTime date, {bool isSeconds = true}) { + final unix = date.millisecondsSinceEpoch; + if (Platform.isAndroid) { + return isSeconds ? (unix ~/ 1000).toString() : unix.toString(); + } else if (Platform.isIOS || Platform.isMacOS) { + final date = (unix / 1000) - 978307200; + // 978307200 is 2001-01-01 00:00:00 UTC, the 0 of the NSDate. + // The NSDate will be converted to CAST(unix - 978307200, "NSDate") in NSPredicate. + final dateStr = date.toStringAsFixed(6); + return 'CAST($dateStr, "NSDate")'; + } else { + throw UnsupportedError('Unsupported platform with date'); + } + } +} diff --git a/lib/src/utils/convert_utils.dart b/lib/src/utils/convert_utils.dart index c0117aa8..34c9d98c 100644 --- a/lib/src/utils/convert_utils.dart +++ b/lib/src/utils/convert_utils.dart @@ -2,7 +2,8 @@ // Use of this source code is governed by an Apache license that can be found // in the LICENSE file. -import '../filter/filter_option_group.dart'; +import '../filter/base_filter.dart'; +import '../filter/classical/filter_option_group.dart'; import '../types/entity.dart'; import '../types/types.dart'; @@ -12,7 +13,7 @@ class ConvertUtils { static List convertToPathList( Map data, { required RequestType type, - FilterOptionGroup? optionGroup, + PMFilter? optionGroup, }) { final List result = []; final List> list = @@ -46,7 +47,7 @@ class ConvertUtils { static AssetPathEntity convertMapToPath( Map data, { required RequestType type, - FilterOptionGroup? optionGroup, + PMFilter? optionGroup, }) { final int? modified = data['modified'] as int?; final DateTime? lastModified = modified != null diff --git a/pubspec.yaml b/pubspec.yaml index a9b03402..0a7f2684 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: photo_manager description: A Flutter plugin that provides assets abstraction management APIs on Android, iOS, and macOS. repository: https://github.com/fluttercandies/flutter_photo_manager -version: 2.5.2 +version: 2.6.0 environment: sdk: ">=2.13.0 <3.0.0"