first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
@preconcurrency import SDWebImage
import ExpoModulesCore
class BlurhashLoader: NSObject, SDImageLoader {
typealias ImageLoaderCompletedBlock = @Sendable (UIImage?, Data?, (any Error)?, Bool) -> Void
// MARK: - SDImageLoader
func canRequestImage(for url: URL?) -> Bool {
return url?.scheme == "blurhash"
}
func requestImage(
with url: URL?,
options: SDWebImageOptions = [],
context: [SDWebImageContextOption: Any]?,
progress progressBlock: SDImageLoaderProgressBlock?,
completed completedBlock: ImageLoaderCompletedBlock? = nil
) -> SDWebImageOperation? {
guard let url else {
let error = makeNSError(description: "URL provided to BlurhashLoader is missing")
completedBlock?(nil, nil, error, false)
return nil
}
guard let source = context?[ImageView.contextSourceKey] as? ImageSource else {
let error = makeNSError(description: "Image source was not provided to the context")
completedBlock?(nil, nil, error, false)
return nil
}
// The URI looks like this: blurhash:/WgF}G?az0fs.x[jat7xFRjNHt6s.4;oe-:RkVtkCi^Nbo|xZRjWB
// Which means that the `pathComponents[0]` is `/` and we need to skip it to get the hash.
let blurhash = url.pathComponents[1]
let size = CGSize(width: source.width, height: source.height)
Task(priority: .high) {
let image = image(fromBlurhash: blurhash, size: size)
await MainActor.run {
if let image {
completedBlock?(UIImage(cgImage: image), nil, nil, true)
} else {
let error = makeNSError(description: "Unable to generate an image from the given blurhash")
completedBlock?(nil, nil, error, false)
}
}
}
return nil
}
func shouldBlockFailedURL(with url: URL, error: Error) -> Bool {
// If the algorithm failed to generate an image from the url,
// it's not possible that next time it will work :)
return true
}
}

View File

@@ -0,0 +1,153 @@
import Photos
import Dispatch
import SDWebImage
import ExpoModulesCore
/**
A custom loader for assets from the Photo Library. It handles all urls with the `ph` scheme.
*/
final class PhotoLibraryAssetLoader: NSObject, SDImageLoader {
// MARK: - SDImageLoader
func canRequestImage(for url: URL?) -> Bool {
return isPhotoLibraryAssetUrl(url)
}
func requestImage(
with url: URL?,
options: SDWebImageOptions = [],
context: SDWebImageContext?,
progress progressBlock: SDImageLoaderProgressBlock?,
completed completedBlock: SDImageLoaderCompletedBlock? = nil
) -> SDWebImageOperation? {
guard isPhotoLibraryStatusAuthorized() else {
let error = makeNSError(description: "Unauthorized access to the Photo Library")
completedBlock?(nil, nil, error, false)
return nil
}
let operation = PhotoLibraryAssetLoaderOperation()
DispatchQueue.global(qos: .userInitiated).async {
guard let url = url, let assetLocalIdentifier = assetLocalIdentifier(fromUrl: url) else {
let error = makeNSError(description: "Unable to obtain the asset identifier from the url: '\(String(describing: url?.absoluteString))'")
completedBlock?(nil, nil, error, false)
return
}
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetLocalIdentifier], options: .none).firstObject else {
let error = makeNSError(description: "Asset with identifier '\(assetLocalIdentifier)' not found in the Photo Library")
completedBlock?(nil, nil, error, false)
return
}
operation.requestId = requestAsset(
asset,
url: url,
context: context,
progressBlock: progressBlock,
completedBlock: completedBlock
)
}
return operation
}
func shouldBlockFailedURL(with url: URL, error: Error) -> Bool {
// The lack of permission is one of the reasons of failed request,
// but in that single case we don't want to blacklist the url as
// the permission might be granted later and then the retry should be possible.
return isPhotoLibraryStatusAuthorized()
}
}
/**
Returns a bool value whether the given url references the Photo Library asset.
*/
internal func isPhotoLibraryAssetUrl(_ url: URL?) -> Bool {
return url?.scheme == "ph"
}
/**
Returns the local identifier of the asset from the given `ph://` url.
These urls have the form of "ph://26687849-33F9-4402-8EC0-A622CD011D70",
where the asset local identifier is used as the host part.
*/
private func assetLocalIdentifier(fromUrl url: URL) -> String? {
return url.host
}
/**
Checks whether the app is authorized to read the Photo Library.
*/
private func isPhotoLibraryStatusAuthorized() -> Bool {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
return status == .authorized || status == .limited
}
/**
Requests the image of the given asset object and returns the request identifier.
*/
private func requestAsset(
_ asset: PHAsset,
url: URL,
context: SDWebImageContext?,
progressBlock: SDImageLoaderProgressBlock?,
completedBlock: SDImageLoaderCompletedBlock?
) -> PHImageRequestID {
let options = PHImageRequestOptions()
options.isSynchronous = false
options.version = .current
options.deliveryMode = .highQualityFormat
options.resizeMode = .fast
options.normalizedCropRect = .zero
options.isNetworkAccessAllowed = true
if let progressBlock = progressBlock {
options.progressHandler = { progress, _, _, _ in
// The `progress` is a double from 0.0 to 1.0, but the loader needs integers so we map it to 0...100 range
let progressPercentage = Int((progress * 100.0).rounded())
progressBlock(progressPercentage, 100, url)
}
}
var targetSize = PHImageManagerMaximumSize
// We compute the minimal size required to display the image to avoid having to downsample it later
if let scale = context?[ImageView.screenScaleKey] as? Double,
let containerSize = context?[ImageView.frameSizeKey] as? CGSize,
let contentFit = context?[ImageView.contentFitKey] as? ContentFit {
targetSize = idealSize(
contentPixelSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight),
containerSize: containerSize,
scale: scale,
contentFit: contentFit
).rounded(.up) * scale
}
return PHImageManager.default().requestImage(
for: asset,
targetSize: targetSize,
contentMode: .aspectFit,
options: options,
resultHandler: { image, info in
// This value can be `true` only when network access is allowed and the photo is stored in the iCloud.
let isDegraded: Bool = info?[PHImageResultIsDegradedKey] as? Bool ?? false
completedBlock?(image, nil, nil, !isDegraded)
}
)
}
/**
Loader operation specialized for the Photo Library by keeping the request identifier.
*/
private class PhotoLibraryAssetLoaderOperation: NSObject, SDWebImageOperation {
var canceled: Bool = false
var requestId: PHImageRequestID?
// MARK: - SDWebImageOperation
func cancel() {
if let requestId = requestId {
PHImageManager.default().cancelImageRequest(requestId)
}
canceled = true
}
}

View File

@@ -0,0 +1,57 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import SDWebImage
import ExpoModulesCore
class SFSymbolLoader: NSObject, SDImageLoader {
// MARK: - SDImageLoader
func canRequestImage(for url: URL?) -> Bool {
return url?.scheme == "sf"
}
func requestImage(
with url: URL?,
options: SDWebImageOptions = [],
context: [SDWebImageContextOption: Any]?,
progress progressBlock: SDImageLoaderProgressBlock?,
completed completedBlock: SDImageLoaderCompletedBlock? = nil
) -> SDWebImageOperation? {
guard let url else {
let error = makeNSError(description: "URL provided to SFSymbolLoader is missing")
completedBlock?(nil, nil, error, false)
return nil
}
// The URI looks like this: sf:/star.fill
// pathComponents[0] is `/`, pathComponents[1] is the symbol name
guard url.pathComponents.count > 1 else {
let error = makeNSError(description: "SF Symbol name is missing from the URL")
completedBlock?(nil, nil, error, false)
return nil
}
let symbolName = url.pathComponents[1]
// Use a large fixed point size for high quality, the image view will scale it down.
// Note: For weight configuration, use the symbolWeight prop on Image component.
// This loader is mainly used for prefetching where weight isn't critical.
let configuration = UIImage.SymbolConfiguration(pointSize: 100, weight: .regular)
guard let image = UIImage(systemName: symbolName, withConfiguration: configuration) else {
let error = makeNSError(description: "Unable to create SF Symbol image for '\(symbolName)'")
completedBlock?(nil, nil, error, false)
return nil
}
// Return as template image so tintColor prop works correctly
let templateImage = image.withRenderingMode(.alwaysTemplate)
completedBlock?(templateImage, nil, nil, true)
return nil
}
func shouldBlockFailedURL(with url: URL, error: Error) -> Bool {
// If the symbol doesn't exist, it won't exist on subsequent attempts
return true
}
}

View File

@@ -0,0 +1,55 @@
@preconcurrency import SDWebImage
import ExpoModulesCore
class ThumbhashLoader: NSObject, SDImageLoader {
typealias ImageLoaderCompletedBlock = @Sendable (UIImage?, Data?, (any Error)?, Bool) -> Void
// MARK: - SDImageLoader
func canRequestImage(for url: URL?) -> Bool {
return url?.scheme == "thumbhash"
}
func requestImage(
with url: URL?,
options: SDWebImageOptions = [],
context: [SDWebImageContextOption: Any]?,
progress progressBlock: SDImageLoaderProgressBlock?,
completed completedBlock: ImageLoaderCompletedBlock? = nil
) -> SDWebImageOperation? {
guard let url else {
let error = makeNSError(description: "URL provided to ThumbhashLoader is missing")
completedBlock?(nil, nil, error, false)
return nil
}
// The URI looks like this: thumbhash:/3OcRJYB4d3h\iIeHeEh3eIhw+j2w
// ThumbHash may include slashes which could break the structure of the URL, so we replace them
// with backslashes on the JS side and revert them back to slashes here, before generating the image.
var thumbhash = url.pathComponents[1].replacingOccurrences(of: "\\", with: "/")
// Thumbhashes with transparency cause the conversion to data to fail, padding the thumbhash string to correct length fixes that
let remainder = thumbhash.count % 4
if remainder > 0 {
thumbhash = thumbhash.padding(toLength: thumbhash.count + 4 - remainder, withPad: "=", startingAt: 0)
}
guard !thumbhash.isEmpty, let thumbhashData = Data(base64Encoded: thumbhash, options: .ignoreUnknownCharacters) else {
let error = makeNSError(description: "URL provided to ThumbhashLoader is invalid")
completedBlock?(nil, nil, error, false)
return nil
}
Task(priority: .high) {
let image = image(fromThumbhash: thumbhashData)
await MainActor.run {
completedBlock?(image, nil, nil, true)
}
}
return nil
}
func shouldBlockFailedURL(with url: URL, error: Error) -> Bool {
return true
}
}