diff --git a/android/src/main/kotlin/com/morbit/photogallery/PhotoGalleryPlugin.kt b/android/src/main/kotlin/com/morbit/photogallery/PhotoGalleryPlugin.kt index 1a07af0..d544774 100644 --- a/android/src/main/kotlin/com/morbit/photogallery/PhotoGalleryPlugin.kt +++ b/android/src/main/kotlin/com/morbit/photogallery/PhotoGalleryPlugin.kt @@ -10,7 +10,6 @@ import android.graphics.ImageDecoder import android.os.Build import android.provider.MediaStore import android.util.Size -import androidx.annotation.NonNull import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -81,18 +80,18 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { private val executor: ExecutorService = Executors.newSingleThreadExecutor() - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "photo_gallery") val plugin = PhotoGalleryPlugin() plugin.context = flutterPluginBinding.applicationContext channel.setMethodCallHandler(plugin) } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "listAlbums" -> { val mediumType = call.argument("mediumType") @@ -102,6 +101,7 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { ) } } + "listMedia" -> { val albumId = call.argument("albumId") val mediumType = call.argument("mediumType") @@ -114,6 +114,7 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { ) } } + "getMedium" -> { val mediumId = call.argument("mediumId") val mediumType = call.argument("mediumType") @@ -123,6 +124,7 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { ) } } + "getThumbnail" -> { val mediumId = call.argument("mediumId") val mediumType = call.argument("mediumType") @@ -135,6 +137,7 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { ) } } + "getAlbumThumbnail" -> { val albumId = call.argument("albumId") val mediumType = call.argument("mediumType") @@ -148,6 +151,7 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { ) } } + "getFile" -> { val mediumId = call.argument("mediumId") val mediumType = call.argument("mediumType") @@ -158,6 +162,7 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { ) } } + "cleanCache" -> { executor.submit { result.success( @@ -165,6 +170,7 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { ) } } + else -> result.notImplemented() } } @@ -174,9 +180,11 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { imageType -> { listImageAlbums().values.toList() } + videoType -> { listVideoAlbums().values.toList() } + else -> { listAllAlbums().values.toList() } @@ -224,13 +232,10 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } val albumLinkedMap = linkedMapOf>() - albumLinkedMap.put( - allAlbumId, - hashMapOf( - "id" to allAlbumId, - "name" to allAlbumName, - "count" to total - ) + albumLinkedMap[allAlbumId] = hashMapOf( + "id" to allAlbumId, + "name" to allAlbumName, + "count" to total ) albumLinkedMap.putAll(albumHashMap) return albumLinkedMap @@ -278,13 +283,10 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } val albumLinkedMap = linkedMapOf>() - albumLinkedMap.put( - allAlbumId, - hashMapOf( - "id" to allAlbumId, - "name" to allAlbumName, - "count" to total - ) + albumLinkedMap[allAlbumId] = hashMapOf( + "id" to allAlbumId, + "name" to allAlbumName, + "count" to total ) albumLinkedMap.putAll(albumHashMap) return albumLinkedMap @@ -304,18 +306,28 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { return albumMap } - private fun listMedia(mediumType: String?, albumId: String, newest: Boolean, skip: Int?, take: Int?): Map { + private fun listMedia( + mediumType: String?, + albumId: String, + newest: Boolean, + skip: Int?, + take: Int? + ): Map { return when (mediumType) { imageType -> { listImages(albumId, newest, skip, take) } + videoType -> { listVideos(albumId, newest, skip, take) } + else -> { val images = listImages(albumId, newest, null, null)["items"] as List> val videos = listVideos(albumId, newest, null, null)["items"] as List> - var items = (images + videos).sortedWith(compareBy> { it["creationDate"] as Long }.thenBy { it["modifiedDate"] as Long }) + val comparator = compareBy> { it["creationDate"] as Long } + .thenBy { it["modifiedDate"] as Long } + var items = (images + videos).sortedWith(comparator) if (newest) { items = items.reversed() } @@ -452,9 +464,11 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { imageType -> { getImageMedia(mediumId) } + videoType -> { getVideoMedia(mediumId) } + else -> { getImageMedia(mediumId) ?: getVideoMedia(mediumId) } @@ -505,14 +519,22 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { return videoMetadata } - private fun getThumbnail(mediumId: String, mediumType: String?, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { + private fun getThumbnail( + mediumId: String, + mediumType: String?, + width: Int?, + height: Int?, + highQuality: Boolean? + ): ByteArray? { return when (mediumType) { imageType -> { getImageThumbnail(mediumId, width, height, highQuality) } + videoType -> { getVideoThumbnail(mediumId, width, height, highQuality) } + else -> { getImageThumbnail(mediumId, width, height, highQuality) ?: getVideoThumbnail(mediumId, width, height, highQuality) @@ -537,7 +559,9 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { null } } else { - val kind = if (highQuality == true) MediaStore.Images.Thumbnails.MINI_KIND else MediaStore.Images.Thumbnails.MICRO_KIND + val kind = + if (highQuality == true) MediaStore.Images.Thumbnails.MINI_KIND + else MediaStore.Images.Thumbnails.MICRO_KIND MediaStore.Images.Thumbnails.getThumbnail( this.contentResolver, mediumId.toLong(), kind, null @@ -572,7 +596,8 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } } else { val kind = - if (highQuality == true) MediaStore.Video.Thumbnails.MINI_KIND else MediaStore.Video.Thumbnails.MICRO_KIND + if (highQuality == true) MediaStore.Video.Thumbnails.MINI_KIND + else MediaStore.Video.Thumbnails.MICRO_KIND MediaStore.Video.Thumbnails.getThumbnail( this.contentResolver, mediumId.toLong(), kind, null @@ -589,21 +614,36 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { return byteArray } - private fun getAlbumThumbnail(albumId: String, mediumType: String?, newest: Boolean, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { + private fun getAlbumThumbnail( + albumId: String, + mediumType: String?, + newest: Boolean, + width: Int?, + height: Int?, + highQuality: Boolean? + ): ByteArray? { return when (mediumType) { imageType -> { getImageAlbumThumbnail(albumId, newest, width, height, highQuality) } + videoType -> { getVideoAlbumThumbnail(albumId, newest, width, height, highQuality) } + else -> { getAllAlbumThumbnail(albumId, newest, width, height, highQuality) } } } - private fun getImageAlbumThumbnail(albumId: String, newest: Boolean, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { + private fun getImageAlbumThumbnail( + albumId: String, + newest: Boolean, + width: Int?, + height: Int?, + highQuality: Boolean? + ): ByteArray? { return this.context.run { val projection = arrayOf(MediaStore.Images.Media._ID) @@ -621,7 +661,13 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } } - private fun getVideoAlbumThumbnail(albumId: String, newest: Boolean, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { + private fun getVideoAlbumThumbnail( + albumId: String, + newest: Boolean, + width: Int?, + height: Int?, + highQuality: Boolean? + ): ByteArray? { return this.context.run { val projection = arrayOf(MediaStore.Video.Media._ID) @@ -639,7 +685,13 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } } - private fun getAllAlbumThumbnail(albumId: String, newest: Boolean, width: Int?, height: Int?, highQuality: Boolean?): ByteArray? { + private fun getAllAlbumThumbnail( + albumId: String, + newest: Boolean, + width: Int?, + height: Int?, + highQuality: Boolean? + ): ByteArray? { return this.context.run { val imageProjection = arrayOf( MediaStore.Images.Media._ID, @@ -688,20 +740,16 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } if (imageId != null && videoId != null) { - if (imageDateAdded != null && videoDateAdded != null) { - if (newest && imageDateAdded!! < videoDateAdded!! || !newest && imageDateAdded!! > videoDateAdded!!) { - return@run getVideoThumbnail(videoId.toString(), width, height, highQuality) - } else { - return@run getImageThumbnail(imageId.toString(), width, height, highQuality) - } + if (newest && imageDateAdded!! > videoDateAdded!! || !newest && imageDateAdded!! < videoDateAdded!!) { + return@run getImageThumbnail(imageId.toString(), width, height, highQuality) } - if (imageDateModified != null && videoDateModified != null) { - if (newest && imageDateModified!! < videoDateModified!! || !newest && imageDateModified!! > videoDateModified!!) { - return@run getVideoThumbnail(videoId.toString(), width, height, highQuality) - } else { - return@run getImageThumbnail(imageId.toString(), width, height, highQuality) - } + if (newest && imageDateAdded!! < videoDateAdded!! || !newest && imageDateAdded!! > videoDateAdded!!) { + return@run getVideoThumbnail(videoId.toString(), width, height, highQuality) } + if (newest && imageDateModified!! >= videoDateModified!! || !newest && imageDateModified!! <= videoDateModified!!) { + return@run getImageThumbnail(imageId.toString(), width, height, highQuality) + } + return@run getVideoThumbnail(videoId.toString(), width, height, highQuality) } if (imageId != null) { @@ -805,9 +853,11 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { imageType -> { getImageFile(mediumId, mimeType = mimeType) } + videoType -> { getVideoFile(mediumId) } + else -> { getImageFile(mediumId, mimeType = mimeType) ?: getVideoFile(mediumId) } @@ -874,10 +924,12 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { val bitmap: Bitmap? = this.context.run { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { try { - ImageDecoder.decodeBitmap(ImageDecoder.createSource( - this.contentResolver, - ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediumId.toLong()) - )) + ImageDecoder.decodeBitmap( + ImageDecoder.createSource( + this.contentResolver, + ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediumId.toLong()) + ) + ) } catch (e: Exception) { null } @@ -891,24 +943,39 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { bitmap?.let { val compressFormat: Bitmap.CompressFormat - if (mimeType == "image/jpeg") { - val path = File(getCachePath(), "$mediumId.jpeg") - val out = FileOutputStream(path) - compressFormat = Bitmap.CompressFormat.JPEG - it.compress(compressFormat, 100, out) - return path.absolutePath - } else if (mimeType == "image/png") { - val path = File(getCachePath(), "$mediumId.png") - val out = FileOutputStream(path) - compressFormat = Bitmap.CompressFormat.PNG - it.compress(compressFormat, 100, out) - return path.absolutePath - } else if (mimeType == "image/webp") { - val path = File(getCachePath(), "$mediumId.webp") - val out = FileOutputStream(path) - compressFormat = Bitmap.CompressFormat.WEBP_LOSSLESS - it.compress(compressFormat, 100, out) - return path.absolutePath + when (mimeType) { + "image/jpeg" -> { + val path = File(getCachePath(), "$mediumId.jpeg") + val out = FileOutputStream(path) + compressFormat = Bitmap.CompressFormat.JPEG + it.compress(compressFormat, 100, out) + return path.absolutePath + } + + "image/png" -> { + val path = File(getCachePath(), "$mediumId.png") + val out = FileOutputStream(path) + compressFormat = Bitmap.CompressFormat.PNG + it.compress(compressFormat, 100, out) + return path.absolutePath + } + + "image/webp" -> { + val path = File(getCachePath(), "$mediumId.webp") + val out = FileOutputStream(path) + compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Bitmap.CompressFormat.WEBP_LOSSLESS + } + else { + Bitmap.CompressFormat.WEBP + } + it.compress(compressFormat, 100, out) + return path.absolutePath + } + + else -> { + return null + } } } @@ -1013,7 +1080,7 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { } } - private fun getCachePath(): File? { + private fun getCachePath(): File { return this.context.run { val cachePath = File(this.cacheDir, "photo_gallery") if (!cachePath.exists()) { @@ -1025,6 +1092,6 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { private fun cleanCache() { val cachePath = getCachePath() - cachePath?.deleteRecursively() + cachePath.deleteRecursively() } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 0b25270..08ac933 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -83,8 +83,8 @@ class _MyAppState extends State { ...?_albums?.map( (album) => GestureDetector( onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => AlbumPage(album))), + MaterialPageRoute(builder: (context) => AlbumPage(album)), + ), child: Column( children: [ ClipRRect( @@ -95,8 +95,7 @@ class _MyAppState extends State { width: gridWidth, child: FadeInImage( fit: BoxFit.cover, - placeholder: - MemoryImage(kTransparentImage), + placeholder: MemoryImage(kTransparentImage), image: AlbumThumbnailProvider( album: album, highQuality: true, @@ -186,8 +185,9 @@ class AlbumPageState extends State { children: [ ...?_media?.map( (medium) => GestureDetector( - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ViewerPage(medium))), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ViewerPage(medium)), + ), child: Container( color: Colors.grey[300], child: FadeInImage( @@ -293,9 +293,7 @@ class _VideoProviderState extends State { TextButton( onPressed: () { setState(() { - _controller!.value.isPlaying - ? _controller!.pause() - : _controller!.play(); + _controller!.value.isPlaying ? _controller!.pause() : _controller!.play(); }); }, child: Icon( diff --git a/ios/Classes/SwiftPhotoGalleryPlugin.swift b/ios/Classes/SwiftPhotoGalleryPlugin.swift index bcf2bde..fc474f6 100644 --- a/ios/Classes/SwiftPhotoGalleryPlugin.swift +++ b/ios/Classes/SwiftPhotoGalleryPlugin.swift @@ -34,7 +34,8 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { mediumId: mediumId, completion: { (data: [String: Any?]?, error: Error?) -> Void in result(data) - }) + } + ) } else if(call.method == "getThumbnail") { let arguments = call.arguments as! Dictionary @@ -49,7 +50,8 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { highQuality: highQuality, completion: { (data: Data?, error: Error?) -> Void in result(data) - }) + } + ) } else if(call.method == "getAlbumThumbnail") { let arguments = call.arguments as! Dictionary @@ -68,7 +70,8 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { highQuality: highQuality, completion: { (data: Data?, error: Error?) -> Void in result(data) - }) + } + ) } else if(call.method == "getFile") { let arguments = call.arguments as! Dictionary @@ -79,7 +82,8 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { mimeType: mimeType, completion: { (filepath: String?, error: Error?) -> Void in result(filepath?.replacingOccurrences(of: "file://", with: "")) - }) + } + ) } else if(call.method == "cleanCache") { cleanCache() @@ -254,7 +258,8 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { } let bytes = image.jpegData(compressionQuality: CGFloat(70)) completion(bytes, nil) - }) + } + ) return } @@ -312,7 +317,8 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { } let bytes = image.jpegData(compressionQuality: CGFloat(80)) completion(bytes, nil) - }) + } + ) return } @@ -366,25 +372,28 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { } ) } else if(asset.mediaType == PHAssetMediaType.video - || asset.mediaType == PHAssetMediaType.audio) { + || asset.mediaType == PHAssetMediaType.audio) { let options = PHVideoRequestOptions() options.version = .current options.deliveryMode = .highQualityFormat options.isNetworkAccessAllowed = true - manager.requestAVAsset(forVideo: asset, options: options, resultHandler: { (avAsset, avAudioMix, info) in - DispatchQueue.main.async(execute: { - do { - let avAsset = avAsset as? AVURLAsset - let data = try Data(contentsOf: avAsset!.url) - let fileExt = self.extractFileExtensionFromAsset(asset: asset) - let filepath = self.exportPathForAsset(asset: asset, ext: fileExt) - try! data.write(to: filepath, options: .atomic) - completion(filepath.absoluteString, nil) - } catch { - completion(nil, NSError(domain: "photo_gallery", code: 500, userInfo: nil)) - } - }) + manager.requestAVAsset( + forVideo: asset, + options: options, + resultHandler: { (avAsset, avAudioMix, info) in + DispatchQueue.main.async(execute: { + do { + let avAsset = avAsset as? AVURLAsset + let data = try Data(contentsOf: avAsset!.url) + let fileExt = self.extractFileExtensionFromAsset(asset: asset) + let filepath = self.exportPathForAsset(asset: asset, ext: fileExt) + try! data.write(to: filepath, options: .atomic) + completion(filepath.absoluteString, nil) + } catch { + completion(nil, NSError(domain: "photo_gallery", code: 500, userInfo: nil)) + } + }) } ) } @@ -406,11 +415,11 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { return nil } } - + private func getMediumFromAsset(asset: PHAsset) -> [String: Any?] { let mimeType = self.extractMimeTypeFromAsset(asset: asset) let filename = self.extractFilenameFromAsset(asset: asset) - let size = self.extractSizeFromAsset(asset: asset) + let size = self.extractSizeFromAsset(asset: asset) return [ "id": asset.localIdentifier, "filename": filename, @@ -419,7 +428,7 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { "mimeType": mimeType, "height": asset.pixelHeight, "width": asset.pixelWidth, - "size": size, + "size": size, "duration": NSInteger(asset.duration * 1000), "creationDate": (asset.creationDate != nil) ? NSInteger(asset.creationDate!.timeIntervalSince1970 * 1000) : nil, "modifiedDate": (asset.modificationDate != nil) ? NSInteger(asset.modificationDate!.timeIntervalSince1970 * 1000) : nil @@ -429,7 +438,7 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { private func getMediumFromAssetAsync(asset: PHAsset, completion: @escaping ([String : Any?]?, Error?) -> Void) -> Void { let mimeType = self.extractMimeTypeFromAsset(asset: asset) let filename = self.extractFilenameFromAsset(asset: asset) - let size = self.extractSizeFromAsset(asset: asset) + let size = self.extractSizeFromAsset(asset: asset) let manager = PHImageManager.default() manager.requestImageData( for: asset, @@ -443,7 +452,7 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { "mimeType": mimeType, "height": asset.pixelHeight, "width": asset.pixelWidth, - "size": size, + "size": size, "orientation": self.toOrientationValue(orientation: orientation), "duration": NSInteger(asset.duration * 1000), "creationDate": (asset.creationDate != nil) ? NSInteger(asset.creationDate!.timeIntervalSince1970 * 1000) : nil, @@ -452,7 +461,7 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { } ) } - + private func exportPathForAsset(asset: PHAsset, ext: String) -> URL { let mediumId = asset.localIdentifier .replacingOccurrences(of: "/", with: "__") @@ -511,7 +520,7 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { } return NSPredicate(format: "mediaType = %d", swiftType.rawValue) } - + private func extractFileExtensionFromUTI(uti: String?) -> String { guard let assetUTI = uti else { return "" @@ -561,14 +570,14 @@ public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { } return asset.value(forKey: "filename") as? String } - + private func extractSizeFromAsset(asset: PHAsset) -> Int64? { - let resources = PHAssetResource.assetResources(for: asset) - guard let resource = resources.first, - let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong else { - return nil - } - return Int64(bitPattern: UInt64(unsignedInt64)) + let resources = PHAssetResource.assetResources(for: asset) + guard let resource = resources.first, + let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong else { + return nil + } + return Int64(bitPattern: UInt64(unsignedInt64)) } private func extractTitleFromFilename(filename: String?) -> String? {