Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MediaAssetsContext",
module_name = "MediaAssetsContext",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,308 @@
import Foundation
import UIKit
import Photos
import SwiftSignalKit
private let imageManager: PHCachingImageManager = {
let imageManager = PHCachingImageManager()
imageManager.allowsCachingHighQualityImages = false
return imageManager
}()
private let assetsQueue = Queue()
public final class AssetDownloadManager {
private final class DownloadingAssetContext {
let identifier: String
let updated: () -> Void
var status: AssetDownloadStatus = .none
var disposable: Disposable?
init(identifier: String, updated: @escaping () -> Void) {
self.identifier = identifier
self.updated = updated
}
deinit {
self.disposable?.dispose()
}
}
private let queue = Queue()
private var currentAssetContext: DownloadingAssetContext?
public init() {
}
deinit {
}
public func download(asset: PHAsset) {
self.cancelAllDownloads()
let queue = self.queue
let identifier = asset.localIdentifier
let assetContext = DownloadingAssetContext(identifier: identifier, updated: { [weak self] in
queue.async {
guard let self else {
return
}
if let currentAssetContext = self.currentAssetContext, currentAssetContext.identifier == identifier, let bag = self.progressObserverContexts[identifier] {
for f in bag.copyItems() {
f(currentAssetContext.status)
}
}
}
})
self.currentAssetContext = assetContext
assetContext.disposable = (downloadAssetMediaData(asset)
|> deliverOn(queue)).start(next: { [weak self] status in
guard let self else {
return
}
if let currentAssetContext = self.currentAssetContext, currentAssetContext.identifier == identifier {
currentAssetContext.status = status
currentAssetContext.updated()
}
})
}
public func cancelAllDownloads() {
if let currentAssetContext = self.currentAssetContext {
currentAssetContext.status = .none
currentAssetContext.updated()
currentAssetContext.disposable?.dispose()
self.queue.justDispatch {
if self.currentAssetContext === currentAssetContext {
self.currentAssetContext = nil
}
}
}
}
public func cancel(identifier: String) {
if let currentAssetContext = self.currentAssetContext, currentAssetContext.identifier == identifier {
currentAssetContext.status = .none
currentAssetContext.updated()
currentAssetContext.disposable?.dispose()
self.queue.justDispatch {
if self.currentAssetContext === currentAssetContext {
self.currentAssetContext = nil
}
}
}
}
private var progressObserverContexts: [String: Bag<(AssetDownloadStatus) -> Void>] = [:]
private func downloadProgress(identifier: String, next: @escaping (AssetDownloadStatus) -> Void) -> Disposable {
let bag: Bag<(AssetDownloadStatus) -> Void>
if let current = self.progressObserverContexts[identifier] {
bag = current
} else {
bag = Bag()
self.progressObserverContexts[identifier] = bag
}
let index = bag.add(next)
if let currentAssetContext = self.currentAssetContext, currentAssetContext.identifier == identifier {
next(currentAssetContext.status)
} else {
next(.none)
}
let queue = self.queue
return ActionDisposable { [weak self, weak bag] in
queue.async {
guard let `self` = self else {
return
}
if let bag = bag, let listBag = self.progressObserverContexts[identifier], listBag === bag {
bag.remove(index)
if bag.isEmpty {
self.progressObserverContexts.removeValue(forKey: identifier)
}
}
}
}
}
public func downloadProgress(identifier: String) -> Signal<AssetDownloadStatus, NoError> {
return Signal { [weak self] subscriber in
if let self {
return self.downloadProgress(identifier: identifier, next: { status in
subscriber.putNext(status)
if case .completed = status {
subscriber.putCompletion()
}
})
} else {
return EmptyDisposable
}
} |> runOn(self.queue)
}
}
public func checkIfAssetIsLocal(_ asset: PHAsset) -> Signal<Bool, NoError> {
if asset.isLocallyAvailable == true {
return .single(true)
}
return Signal { subscriber in
let requestId: PHImageRequestID
if case .video = asset.mediaType {
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = false
requestId = imageManager.requestAVAsset(forVideo: asset, options: options) { asset, _, _ in
subscriber.putNext(asset != nil)
subscriber.putCompletion()
}
} else {
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = false
if #available(iOS 13, *) {
requestId = imageManager.requestImageDataAndOrientation(for: asset, options: options) { data, _, _, _ in
subscriber.putNext(data != nil)
subscriber.putCompletion()
}
} else {
requestId = imageManager.requestImageData(for: asset, options: options) { data, _, _, _ in
subscriber.putNext(data != nil)
subscriber.putCompletion()
}
}
}
return ActionDisposable {
imageManager.cancelImageRequest(requestId)
}
}
}
public enum AssetDownloadStatus {
case none
case progress(Float)
case completed
}
private func downloadAssetMediaData(_ asset: PHAsset) -> Signal<AssetDownloadStatus, NoError> {
return Signal { subscriber in
let requestId: PHImageRequestID
if case .video = asset.mediaType {
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
options.progressHandler = { progress, _, _, _ in
subscriber.putNext(.progress(Float(progress)))
}
subscriber.putNext(.progress(0.0))
requestId = imageManager.requestAVAsset(forVideo: asset, options: options) { asset, _, _ in
if asset != nil {
subscriber.putNext(.completed)
} else {
subscriber.putNext(.none)
}
subscriber.putCompletion()
}
} else {
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
options.progressHandler = { progress, _, _, _ in
subscriber.putNext(.progress(Float(progress)))
}
subscriber.putNext(.progress(0.0))
if #available(iOS 13, *) {
requestId = imageManager.requestImageDataAndOrientation(for: asset, options: options) { data, _, _, _ in
if data != nil {
subscriber.putNext(.completed)
} else {
subscriber.putNext(.none)
}
subscriber.putCompletion()
}
} else {
requestId = imageManager.requestImageData(for: asset, options: options) { data, _, _, _ in
if data != nil {
subscriber.putNext(.completed)
} else {
subscriber.putNext(.none)
}
subscriber.putCompletion()
}
}
}
return ActionDisposable {
imageManager.cancelImageRequest(requestId)
}
}
}
public func assetImage(fetchResult: PHFetchResult<PHAsset>, index: Int, targetSize: CGSize, exact: Bool, deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic, synchronous: Bool = false) -> Signal<UIImage?, NoError> {
let asset = fetchResult[index]
return assetImage(asset: asset, targetSize: targetSize, exact: exact, deliveryMode: deliveryMode, synchronous: synchronous)
}
public func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool, deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic, synchronous: Bool = false) -> Signal<UIImage?, NoError> {
return Signal { subscriber in
let options = PHImageRequestOptions()
options.deliveryMode = deliveryMode
if exact {
options.resizeMode = .exact
}
options.isSynchronous = synchronous
options.isNetworkAccessAllowed = true
let token = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
var degraded = false
if let info = info {
if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled {
subscriber.putCompletion()
return
}
if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue {
degraded = true
}
}
if let image = image {
subscriber.putNext(image)
if !degraded || deliveryMode == .fastFormat {
subscriber.putCompletion()
}
}
}
return ActionDisposable {
imageManager.cancelImageRequest(token)
}
}
}
public func assetVideo(fetchResult: PHFetchResult<PHAsset>, index: Int) -> Signal<AVAsset?, NoError> {
return Signal { subscriber in
let asset = fetchResult[index]
let options = PHVideoRequestOptions()
let token = imageManager.requestAVAsset(forVideo: asset, options: options) { (avAsset, _, info) in
if let avAsset = avAsset {
subscriber.putNext(avAsset)
subscriber.putCompletion()
}
}
return ActionDisposable {
imageManager.cancelImageRequest(token)
}
}
}
extension PHAsset {
var isLocallyAvailable: Bool? {
let resourceArray = PHAssetResource.assetResources(for: self)
return resourceArray.first?.value(forKey: "locallyAvailable") as? Bool
}
}
@@ -0,0 +1,131 @@
import Foundation
import UIKit
import SwiftSignalKit
import Photos
import AVFoundation
public final class MediaAssetsContext: NSObject, PHPhotoLibraryChangeObserver {
private let assetType: PHAssetMediaType?
private var registeredChangeObserver = false
private let changeSink = ValuePipe<PHChange>()
private let mediaAccessSink = ValuePipe<PHAuthorizationStatus>()
private let cameraAccessSink = ValuePipe<AVAuthorizationStatus?>()
public init(assetType: PHAssetMediaType? = nil) {
self.assetType = assetType
super.init()
if PHPhotoLibrary.authorizationStatus() == .authorized {
PHPhotoLibrary.shared().register(self)
self.registeredChangeObserver = true
}
}
deinit {
if self.registeredChangeObserver {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
}
public func photoLibraryDidChange(_ changeInstance: PHChange) {
self.changeSink.putNext(changeInstance)
}
public func fetchAssets(_ collection: PHAssetCollection) -> Signal<PHFetchResult<PHAsset>, NoError> {
let options = PHFetchOptions()
if let assetType = self.assetType {
options.predicate = NSPredicate(format: "mediaType = %d", assetType.rawValue)
}
let initialFetchResult = PHAsset.fetchAssets(in: collection, options: options)
let fetchResult = Atomic<PHFetchResult<PHAsset>>(value: initialFetchResult)
return .single(initialFetchResult)
|> then(
self.changeSink.signal()
|> mapToSignal { change in
if let updatedFetchResult = change.changeDetails(for: fetchResult.with { $0 })?.fetchResultAfterChanges {
let _ = fetchResult.modify { _ in return updatedFetchResult }
return .single(updatedFetchResult)
} else {
return .complete()
}
}
)
}
public func fetchAssetsCollections(_ type: PHAssetCollectionType) -> Signal<PHFetchResult<PHAssetCollection>, NoError> {
let initialFetchResult = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
let fetchResult = Atomic<PHFetchResult<PHAssetCollection>>(value: initialFetchResult)
return .single(initialFetchResult)
|> then(
self.changeSink.signal()
|> mapToSignal { change in
if let updatedFetchResult = change.changeDetails(for: fetchResult.with { $0 })?.fetchResultAfterChanges {
let _ = fetchResult.modify { _ in return updatedFetchResult }
return .single(updatedFetchResult)
} else {
return .complete()
}
}
)
}
public func recentAssets() -> Signal<PHFetchResult<PHAsset>?, NoError> {
let collections = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)
if let collection = collections.firstObject {
return fetchAssets(collection)
|> map(Optional.init)
} else {
return .single(nil)
}
}
public func mediaAccess() -> Signal<PHAuthorizationStatus, NoError> {
let initialStatus: PHAuthorizationStatus
if #available(iOS 14.0, *) {
initialStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
} else {
initialStatus = PHPhotoLibrary.authorizationStatus()
}
return .single(initialStatus)
|> then(
self.mediaAccessSink.signal()
)
}
public func requestMediaAccess(completion: @escaping () -> Void = {}) -> Void {
PHPhotoLibrary.requestAuthorization { [weak self] status in
Queue.mainQueue().async {
completion()
}
self?.mediaAccessSink.putNext(status)
}
}
public func cameraAccess() -> Signal<AVAuthorizationStatus?, NoError> {
#if targetEnvironment(simulator)
return .single(.authorized)
#else
if UIImagePickerController.isSourceTypeAvailable(.camera) {
return .single(AVCaptureDevice.authorizationStatus(for: .video))
|> then(
self.cameraAccessSink.signal()
)
} else {
return .single(nil)
}
#endif
}
public func requestCameraAccess() -> Void {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { [weak self] result in
if result {
self?.cameraAccessSink.putNext(.authorized)
} else {
self?.cameraAccessSink.putNext(.denied)
}
})
}
}