mirror of
https://github.com/immich-app/immich.git
synced 2025-08-30 23:02:39 -04:00
ios fixes and improvements
This commit is contained in:
parent
0f772ea875
commit
e639041b84
@ -3,18 +3,22 @@ import Flutter
|
|||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import Photos
|
import Photos
|
||||||
|
|
||||||
struct Request {
|
class Request {
|
||||||
var managerId: Int32?
|
|
||||||
weak var workItem: DispatchWorkItem?
|
weak var workItem: DispatchWorkItem?
|
||||||
var isCancelled = false
|
var isCancelled = false
|
||||||
let callback: (Result<[String: Int64], any Error>) -> Void
|
let callback: (Result<[String: Int64], any Error>) -> Void
|
||||||
|
|
||||||
|
init(callback: @escaping (Result<[String: Int64], any Error>) -> Void) {
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThumbnailApiImpl: ThumbnailApi {
|
class ThumbnailApiImpl: ThumbnailApi {
|
||||||
private static let cacheManager = PHImageManager.default()
|
private static let imageManager = PHImageManager.default()
|
||||||
private static let fetchOptions = {
|
private static let fetchOptions = {
|
||||||
let fetchOptions = PHFetchOptions()
|
let fetchOptions = PHFetchOptions()
|
||||||
fetchOptions.fetchLimit = 1
|
fetchOptions.fetchLimit = 1
|
||||||
|
fetchOptions.wantsIncrementalChangeDetails = false
|
||||||
return fetchOptions
|
return fetchOptions
|
||||||
}()
|
}()
|
||||||
private static let requestOptions = {
|
private static let requestOptions = {
|
||||||
@ -26,38 +30,71 @@ class ThumbnailApiImpl: ThumbnailApi {
|
|||||||
requestOptions.version = .current
|
requestOptions.version = .current
|
||||||
return requestOptions
|
return requestOptions
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated)
|
||||||
|
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
|
||||||
|
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
|
||||||
private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
|
private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
|
||||||
|
|
||||||
private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
|
private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue
|
private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue
|
||||||
private static var requests = [Int64: Request]()
|
private static var requests = [Int64: Request]()
|
||||||
private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
|
private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
|
||||||
|
private static let concurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2)
|
||||||
|
private static let assetCache = {
|
||||||
|
let assetCache = NSCache<NSString, PHAsset>()
|
||||||
|
assetCache.countLimit = 10000
|
||||||
|
return assetCache
|
||||||
|
}()
|
||||||
|
|
||||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
|
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
|
||||||
var request = Request(callback: completion)
|
let request = Request(callback: completion)
|
||||||
let item = DispatchWorkItem {
|
let item = DispatchWorkItem {
|
||||||
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
|
if request.isCancelled {
|
||||||
else {
|
return completion(Self.cancelledResult)
|
||||||
Self.requests[requestId] = nil
|
|
||||||
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request.managerId = Self.cacheManager.requestImage(
|
Self.concurrencySemaphore.wait()
|
||||||
for: asset,
|
defer {
|
||||||
targetSize: CGSize(width: Double(width), height: Double(height)),
|
Self.concurrencySemaphore.signal()
|
||||||
contentMode: .aspectFit,
|
|
||||||
options: Self.requestOptions,
|
|
||||||
resultHandler: { (image, info) -> Void in
|
|
||||||
defer { Self.requests[requestId] = nil }
|
|
||||||
guard let image = image,
|
|
||||||
let cgImage = image.cgImage else {
|
|
||||||
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.isCancelled {
|
if request.isCancelled {
|
||||||
return completion(Self.cancelledResult)
|
return completion(Self.cancelledResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let asset = Self.requestAsset(assetId: assetId)
|
||||||
|
else {
|
||||||
|
Self.removeRequest(requestId: requestId)
|
||||||
|
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.isCancelled {
|
||||||
|
return completion(Self.cancelledResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
var image: UIImage?
|
||||||
|
Self.imageManager.requestImage(
|
||||||
|
for: asset,
|
||||||
|
targetSize: CGSize(width: Double(width), height: Double(height)),
|
||||||
|
contentMode: .aspectFit,
|
||||||
|
options: Self.requestOptions,
|
||||||
|
resultHandler: { (_image, info) -> Void in
|
||||||
|
image = _image
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.isCancelled {
|
||||||
|
return completion(Self.cancelledResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let image = image,
|
||||||
|
let cgImage = image.cgImage else {
|
||||||
|
Self.removeRequest(requestId: requestId)
|
||||||
|
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
|
||||||
|
}
|
||||||
|
|
||||||
let pointer = UnsafeMutableRawPointer.allocate(
|
let pointer = UnsafeMutableRawPointer.allocate(
|
||||||
byteCount: Int(cgImage.width) * Int(cgImage.height) * 4,
|
byteCount: Int(cgImage.width) * Int(cgImage.height) * 4,
|
||||||
alignment: MemoryLayout<UInt8>.alignment
|
alignment: MemoryLayout<UInt8>.alignment
|
||||||
@ -78,6 +115,7 @@ class ThumbnailApiImpl: ThumbnailApi {
|
|||||||
bitmapInfo: Self.bitmapInfo
|
bitmapInfo: Self.bitmapInfo
|
||||||
) else {
|
) else {
|
||||||
pointer.deallocate()
|
pointer.deallocate()
|
||||||
|
Self.removeRequest(requestId: requestId)
|
||||||
return completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil)))
|
return completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,24 +133,45 @@ class ThumbnailApiImpl: ThumbnailApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
|
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
|
||||||
}
|
Self.removeRequest(requestId: requestId)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request.workItem = item
|
request.workItem = item
|
||||||
Self.requests[requestId] = request
|
Self.addRequest(requestId: requestId, request: request)
|
||||||
Self.processingQueue.async(execute: item)
|
Self.processingQueue.async(execute: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelImageRequest(requestId: Int64) {
|
func cancelImageRequest(requestId: Int64) {
|
||||||
guard var request = Self.requests.removeValue(forKey: requestId) else { return }
|
Self.cancelRequest(requestId: requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func addRequest(requestId: Int64, request: Request) -> Void {
|
||||||
|
requestQueue.sync { requests[requestId] = request }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func removeRequest(requestId: Int64) -> Void {
|
||||||
|
requestQueue.sync { requests[requestId] = nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func cancelRequest(requestId: Int64) -> Void {
|
||||||
|
requestQueue.async {
|
||||||
|
guard let request = requests.removeValue(forKey: requestId) else { return }
|
||||||
request.isCancelled = true
|
request.isCancelled = true
|
||||||
guard let item = request.workItem else { return }
|
guard let item = request.workItem else { return }
|
||||||
item.cancel()
|
|
||||||
if item.isCancelled {
|
if item.isCancelled {
|
||||||
request.callback(Self.cancelledResult)
|
request.callback(Self.cancelledResult)
|
||||||
} else if let managerId = request.managerId {
|
|
||||||
Self.cacheManager.cancelImageRequest(managerId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func requestAsset(assetId: String) -> PHAsset? {
|
||||||
|
var asset: PHAsset?
|
||||||
|
assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) }
|
||||||
|
if asset != nil { return asset }
|
||||||
|
|
||||||
|
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
|
||||||
|
else { return nil }
|
||||||
|
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
||||||
|
return asset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user