request cancellation

This commit is contained in:
mertalev
2025-08-06 23:57:33 -04:00
parent cb51300356
commit 86ee4ff822
14 changed files with 558 additions and 180 deletions

View File

@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -70,7 +70,8 @@ class ThumbnailsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol ThumbnailApi {
func getThumbnailBuffer(assetId: String, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void)
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void)
func cancelImageRequest(requestId: Int64) throws
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -79,14 +80,15 @@ class ThumbnailApiSetup {
/// Sets up an instance of `ThumbnailApi` to handle messages through the `binaryMessenger`.
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ThumbnailApi?, messageChannelSuffix: String = "") {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
let getThumbnailBufferChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbnailBuffer\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
let requestImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.requestImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
getThumbnailBufferChannel.setMessageHandler { message, reply in
requestImageChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let assetIdArg = args[0] as! String
let widthArg = args[1] as! Int64
let heightArg = args[2] as! Int64
api.getThumbnailBuffer(assetId: assetIdArg, width: widthArg, height: heightArg) { result in
let requestIdArg = args[1] as! Int64
let widthArg = args[2] as! Int64
let heightArg = args[3] as! Int64
api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
@@ -96,7 +98,22 @@ class ThumbnailApiSetup {
}
}
} else {
getThumbnailBufferChannel.setMessageHandler(nil)
requestImageChannel.setMessageHandler(nil)
}
let cancelImageRequestChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.cancelImageRequest\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
cancelImageRequestChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let requestIdArg = args[0] as! Int64
do {
try api.cancelImageRequest(requestId: requestIdArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
cancelImageRequestChannel.setMessageHandler(nil)
}
}
}

View File

@@ -3,6 +3,13 @@ import Flutter
import MobileCoreServices
import Photos
struct Request {
var managerId: Int32?
var workItem: DispatchWorkItem?
var isCancelled = false
let callback: (Result<[String: Int64], any Error>) -> Void
}
class ThumbnailApiImpl: ThumbnailApi {
private static let cacheManager = PHImageManager.default()
private static let fetchOptions = {
@@ -22,29 +29,45 @@ class ThumbnailApiImpl: ThumbnailApi {
private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue
private static var requests = [Int64: Request]()
private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
func getThumbnailBuffer(assetId: String, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
Self.processingQueue.async {
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
var request = Request(callback: completion)
let item = DispatchWorkItem {
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
else { completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil))); return }
else {
Self.requests[requestId] = nil
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
return
}
Self.cacheManager.requestImage(
request.managerId = Self.cacheManager.requestImage(
for: asset,
targetSize: CGSize(width: Double(width), height: Double(height)),
contentMode: .aspectFit,
options: Self.requestOptions,
resultHandler: { (image, info) -> Void in
defer { Self.requests[requestId] = nil }
guard let image = image,
let cgImage = image.cgImage else {
completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
return
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
}
if request.isCancelled {
return completion(Self.cancelledResult)
}
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: Int(cgImage.width) * Int(cgImage.height) * 4,
alignment: MemoryLayout<UInt8>.alignment
)
if request.isCancelled {
pointer.deallocate()
return completion(Self.cancelledResult)
}
guard let context = CGContext(
data: pointer,
width: cgImage.width,
@@ -55,14 +78,41 @@ class ThumbnailApiImpl: ThumbnailApi {
bitmapInfo: Self.bitmapInfo
) else {
pointer.deallocate()
completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil)))
return
return completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil)))
}
if request.isCancelled {
pointer.deallocate()
return completion(Self.cancelledResult)
}
context.interpolationQuality = .none
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
if request.isCancelled {
pointer.deallocate()
return completion(Self.cancelledResult)
}
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
}
)
}
request.workItem = item
Self.requests[requestId] = request
Self.processingQueue.async(execute: item)
}
func cancelImageRequest(requestId: Int64) {
guard var request = Self.requests.removeValue(forKey: requestId) else { return }
request.isCancelled = true
guard let item = request.workItem else { return }
item.cancel()
if item.isCancelled {
request.callback(Self.cancelledResult)
} else if let managerId = request.managerId {
Self.cacheManager.cancelImageRequest(managerId)
}
}
}