This commit is contained in:
mertalev 2025-07-18 14:41:19 +03:00
parent e1cc8f8fe1
commit 8d163ec932
No known key found for this signature in database
GPG Key ID: DF6ABC77AAD98C95
6 changed files with 39 additions and 196 deletions

View File

@ -15,9 +15,6 @@ import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
private object ThumbnailsPigeonUtils { private object ThumbnailsPigeonUtils {
fun createConnectionError(channelName: String): FlutterError {
return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") }
fun wrapResult(result: Any?): List<Any?> { fun wrapResult(result: Any?): List<Any?> {
return listOf(result) return listOf(result)
} }
@ -98,30 +95,3 @@ interface ThumbnailApi {
} }
} }
} }
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
class PlatformThumbnailApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by PlatformThumbnailApi. */
val codec: MessageCodec<Any?> by lazy {
ThumbnailsPigeonCodec()
}
}
fun getThumbnail(assetIdArg: String, widthArg: Long, heightArg: Long, callback: (Result<ByteArray?>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(assetIdArg, widthArg, heightArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
val output = it[0] as ByteArray?
callback(Result.success(output))
}
} else {
callback(Result.failure(ThumbnailsPigeonUtils.createConnectionError(channelName)))
}
}
}
}

View File

@ -37,10 +37,6 @@ private func wrapError(_ error: Any) -> [Any?] {
] ]
} }
private func createConnectionError(withChannelName channelName: String) -> PigeonError {
return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "")
}
private func isNullish(_ value: Any?) -> Bool { private func isNullish(_ value: Any?) -> Bool {
return value is NSNull || value == nil return value is NSNull || value == nil
} }
@ -104,37 +100,3 @@ class ThumbnailApiSetup {
} }
} }
} }
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
protocol PlatformThumbnailApiProtocol {
func getThumbnail(assetId assetIdArg: String, width widthArg: Int64, height heightArg: Int64, completion: @escaping (Result<FlutterStandardTypedData?, PigeonError>) -> Void)
}
class PlatformThumbnailApi: PlatformThumbnailApiProtocol {
private let binaryMessenger: FlutterBinaryMessenger
private let messageChannelSuffix: String
init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") {
self.binaryMessenger = binaryMessenger
self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
}
var codec: ThumbnailsPigeonCodec {
return ThumbnailsPigeonCodec.shared
}
func getThumbnail(assetId assetIdArg: String, width widthArg: Int64, height heightArg: Int64, completion: @escaping (Result<FlutterStandardTypedData?, PigeonError>) -> Void) {
let channelName: String = "dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
channel.sendMessage([assetIdArg, widthArg, heightArg] as [Any?]) { response in
guard let listResponse = response as? [Any?] else {
completion(.failure(createConnectionError(withChannelName: channelName)))
return
}
if listResponse.count > 1 {
let code: String = listResponse[0] as! String
let message: String? = nilOrValue(listResponse[1])
let details: String? = nilOrValue(listResponse[2])
completion(.failure(PigeonError(code: code, message: message, details: details)))
} else {
let result: FlutterStandardTypedData? = nilOrValue(listResponse[0])
completion(.success(result))
}
}
}
}

View File

@ -1,21 +1,23 @@
import CryptoKit import CryptoKit
import Flutter import Flutter
import Photos
import MobileCoreServices import MobileCoreServices
import Photos
// https://stackoverflow.com/a/55839062 // https://stackoverflow.com/a/55839062
extension UIImage { extension UIImage {
func toData (options: NSDictionary?, type: ImageType) -> Data? { func toData(options: NSDictionary?, type: ImageType) -> Data? {
guard cgImage != nil else { return nil } guard cgImage != nil else { return nil }
return toData(options: options, type: type.value) return toData(options: options, type: type.value)
} }
// about properties: https://developer.apple.com/documentation/imageio/1464962-cgimagedestinationaddimage // about properties: https://developer.apple.com/documentation/imageio/1464962-cgimagedestinationaddimage
func toData (options: NSDictionary?, type: CFString) -> Data? { func toData(options: NSDictionary?, type: CFString) -> Data? {
guard let cgImage = cgImage else { return nil } guard let cgImage = cgImage else { return nil }
return autoreleasepool { () -> Data? in return autoreleasepool { () -> Data? in
let data = NSMutableData() let data = NSMutableData()
guard let imageDestination = CGImageDestinationCreateWithData(data as CFMutableData, type, 1, nil) else { return nil } guard
let imageDestination = CGImageDestinationCreateWithData(data as CFMutableData, type, 1, nil)
else { return nil }
CGImageDestinationAddImage(imageDestination, cgImage, options) CGImageDestinationAddImage(imageDestination, cgImage, options)
CGImageDestinationFinalize(imageDestination) CGImageDestinationFinalize(imageDestination)
return data as Data return data as Data
@ -75,41 +77,10 @@ class ThumbnailApiImpl: ThumbnailApi {
requestOptions.version = .current requestOptions.version = .current
return requestOptions return requestOptions
}() }()
private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent) private static let processingQueue = DispatchQueue(
private static let imageCache = NSCache<NSString, FlutterStandardTypedData>() label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
func requestThumbnail( func getThumbnail(
assetId: String,
width: Int64,
height: Int64,
completion: @escaping (Result<Int32, Error>) -> Void
) {
Self.processingQueue.async {
do {
let asset = try self.getAsset(assetId: assetId)
let requestId = Self.cacheManager.requestImage(
for: asset,
targetSize: CGSize(width: Double(width), height: Double(height)),
contentMode: .aspectFill,
options: Self.requestOptions,
resultHandler: { (image, info) -> Void in
guard let data = image?.toData(options: nil, type: .bmp) else { return }
Self.imageCache.setObject(FlutterStandardTypedData(bytes: data), forKey: assetId as NSString)
}
)
completion(.success(requestId))
} catch {
completion(.failure(PigeonError(code: "", message: "Could not get asset data", details: nil)))
}
}
}
func getThumbnail(assetId assetIdArg: String, width widthArg: Int64, height heightArg: Int64, completion: @escaping (Result<FlutterStandardTypedData?, PigeonError>) -> Void) {
}
func sendThumbnail(
assetId: String, assetId: String,
width: Int64, width: Int64,
height: Int64, height: Int64,
@ -130,15 +101,12 @@ class ThumbnailApiImpl: ThumbnailApi {
} }
) )
} catch { } catch {
completion(.failure(PigeonError(code: "", message: "Could not get asset data", details: nil))) completion(
.failure(PigeonError(code: "", message: "Could not get asset data", details: nil)))
} }
} }
} }
func cancel(requestId: Int32) {
Self.cacheManager.cancelImageRequest(requestId as PHImageRequestID)
}
private func getAsset(assetId: String) throws -> PHAsset { private func getAsset(assetId: String) throws -> PHAsset {
guard guard
let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions) let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions)
@ -148,4 +116,9 @@ class ThumbnailApiImpl: ThumbnailApi {
} }
return asset return asset
} }
// func cancel(requestId: Int32) {
// Self.cacheManager.cancelImageRequest(requestId as PHImageRequestID)
// }
} }

View File

@ -15,17 +15,6 @@ PlatformException _createConnectionError(String channelName) {
); );
} }
List<Object?> wrapResponse(
{Object? result, PlatformException? error, bool empty = false}) {
if (empty) {
return <Object?>[];
}
if (error == null) {
return <Object?>[result];
}
return <Object?>[error.code, error.message, error.details];
}
class _PigeonCodec extends StandardMessageCodec { class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec(); const _PigeonCodec();
@override @override
@ -97,54 +86,3 @@ class ThumbnailApi {
} }
} }
} }
abstract class PlatformThumbnailApi {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
Future<Uint8List?> getThumbnail(String assetId, int width, int height);
static void setUp(
PlatformThumbnailApi? api, {
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
messageChannelSuffix =
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final BasicMessageChannel<
Object?> pigeonVar_channel = BasicMessageChannel<
Object?>(
'dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail$messageChannelSuffix',
pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail was null.');
final List<Object?> args = (message as List<Object?>?)!;
final String? arg_assetId = (args[0] as String?);
assert(arg_assetId != null,
'Argument for dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail was null, expected non-null String.');
final int? arg_width = (args[1] as int?);
assert(arg_width != null,
'Argument for dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail was null, expected non-null int.');
final int? arg_height = (args[2] as int?);
assert(arg_height != null,
'Argument for dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail was null, expected non-null int.');
try {
final Uint8List? output =
await api.getThumbnail(arg_assetId!, arg_width!, arg_height!);
return wrapResponse(result: output);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
}
}

View File

@ -26,7 +26,7 @@ class LocalAlbumThumbnail extends ConsumerWidget {
return ClipRRect( return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Thumbnail(asset: data), child: Thumbnail.fromBaseAsset(asset: data),
); );
}, },
error: (error, stack) { error: (error, stack) {

View File

@ -30,8 +30,8 @@ abstract class ThumbnailApi {
// }); // });
} }
@FlutterApi() // @FlutterApi()
abstract class PlatformThumbnailApi { // abstract class PlatformThumbnailApi {
@async // @async
Uint8List? getThumbnail(String assetId, int width, int height); // Uint8List? getThumbnail(String assetId, int width, int height);
} // }