mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
[WIP] Story collage
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user