2024-05-10 20:57:12 +04:00

376 lines
14 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import TelegramCore
import Postbox
import SwiftSignalKit
import MultiAnimationRenderer
import AnimationCache
import AccountContext
import TelegramUIPreferences
import GenerateStickerPlaceholderImage
import EmojiTextAttachmentView
import LottieAnimationCache
public final class InlineFileIconLayer: MultiAnimationRenderTarget {
private final class Arguments {
let context: InlineFileIconLayer.Context
let userLocation: MediaResourceUserLocation
let file: TelegramMediaFile
let cache: AnimationCache
let renderer: MultiAnimationRenderer
let unique: Bool
let placeholderColor: UIColor
let pointSize: CGSize
let pixelSize: CGSize
init(context: InlineFileIconLayer.Context, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool, placeholderColor: UIColor, pointSize: CGSize, pixelSize: CGSize) {
self.context = context
self.userLocation = userLocation
self.file = file
self.cache = cache
self.renderer = renderer
self.unique = unique
self.placeholderColor = placeholderColor
self.pointSize = pointSize
self.pixelSize = pixelSize
}
}
public enum Context: Equatable {
public final class Custom: Equatable {
public let postbox: Postbox
public let energyUsageSettings: () -> EnergyUsageSettings
public let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>
public init(postbox: Postbox, energyUsageSettings: @escaping () -> EnergyUsageSettings, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>) {
self.postbox = postbox
self.energyUsageSettings = energyUsageSettings
self.resolveInlineStickers = resolveInlineStickers
}
public static func ==(lhs: Custom, rhs: Custom) -> Bool {
if lhs.postbox !== rhs.postbox {
return false
}
return true
}
}
case account(AccountContext)
case custom(Custom)
var postbox: Postbox {
switch self {
case let .account(account):
return account.account.postbox
case let .custom(custom):
return custom.postbox
}
}
var energyUsageSettings: EnergyUsageSettings {
switch self {
case let .account(account):
return account.sharedContext.energyUsageSettings
case let .custom(custom):
return custom.energyUsageSettings()
}
}
func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> {
switch self {
case let .account(account):
return account.engine.stickers.resolveInlineStickers(fileIds: fileIds)
case let .custom(custom):
return custom.resolveInlineStickers(fileIds)
}
}
public static func ==(lhs: Context, rhs: Context) -> Bool {
switch lhs {
case let .account(lhsContext):
if case let .account(rhsContext) = rhs, lhsContext === rhsContext {
return true
} else {
return false
}
case let .custom(custom):
if case .custom(custom) = rhs {
return true
} else {
return false
}
}
}
}
public static let queue = Queue()
public struct Key: Hashable {
public var id: Int64
public var index: Int
public init(id: Int64, index: Int) {
self.id = id
self.index = index
}
}
private let arguments: Arguments?
private var isDisplayingPlaceholder: Bool = false
private var didProcessTintColor: Bool = false
public private(set) var file: TelegramMediaFile?
private var infoDisposable: Disposable?
private var disposable: Disposable?
private var fetchDisposable: Disposable?
private var loadDisposable: Disposable?
private var _contentTintColor: UIColor?
public var contentTintColor: UIColor? {
get {
return self._contentTintColor
}
set(value) {
if self._contentTintColor != value {
self._contentTintColor = value
}
}
}
private var _dynamicColor: UIColor?
public var dynamicColor: UIColor? {
get {
return self._dynamicColor
}
set(value) {
if self._dynamicColor != value {
self._dynamicColor = value
}
}
}
private var currentLoopCount: Int = 0
private var isInHierarchyValue: Bool = false
public convenience init(
context: AccountContext,
userLocation: MediaResourceUserLocation,
attemptSynchronousLoad: Bool,
file: TelegramMediaFile,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
unique: Bool = false,
placeholderColor: UIColor,
pointSize: CGSize,
dynamicColor: UIColor? = nil
) {
self.init(
context: .account(context),
userLocation: userLocation,
attemptSynchronousLoad: attemptSynchronousLoad,
file: file,
cache: cache,
renderer: renderer,
unique: unique,
placeholderColor: placeholderColor,
pointSize: pointSize,
dynamicColor: dynamicColor
)
}
public init(
context: InlineFileIconLayer.Context,
userLocation: MediaResourceUserLocation,
attemptSynchronousLoad: Bool,
file: TelegramMediaFile,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
unique: Bool = false,
placeholderColor: UIColor,
pointSize: CGSize,
dynamicColor: UIColor? = nil
) {
let scale = min(2.0, UIScreenScale)
self.arguments = Arguments(
context: context,
userLocation: userLocation,
file: file,
cache: cache,
renderer: renderer,
unique: unique,
placeholderColor: placeholderColor,
pointSize: pointSize,
pixelSize: CGSize(width: pointSize.width * scale, height: pointSize.height * scale)
)
self._dynamicColor = dynamicColor
super.init()
self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad)
}
override public init(layer: Any) {
self.arguments = nil
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.loadDisposable?.dispose()
self.infoDisposable?.dispose()
self.disposable?.dispose()
self.fetchDisposable?.dispose()
}
override public func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchyValue = true
} else if event == kCAOnOrderOut {
self.isInHierarchyValue = false
}
return nullAction
}
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
guard let arguments = self.arguments else {
return
}
if self.file?.fileId == file.fileId {
return
}
self.file = file
if attemptSynchronousLoad {
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize) {
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: arguments.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: arguments.placeholderColor) {
self.contents = image.cgImage
self.isDisplayingPlaceholder = true
}
}
self.loadAnimation()
} else {
let isTemplate = file.isCustomTemplateEmoji
let pointSize = arguments.pointSize
let placeholderColor = arguments.placeholderColor
let isThumbnailCancelled = Atomic<Bool>(value: false)
self.loadDisposable = arguments.renderer.loadFirstFrame(
target: self,
cache: arguments.cache,
itemId: file.resource.id.stringRepresentation,
size: arguments.pixelSize,
fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in
if !result {
MultiAnimationRendererImpl.firstFrameQueue.async {
let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor)
DispatchQueue.main.async {
guard let strongSelf = self, !isThumbnailCancelled.with({ $0 }) else {
return
}
if let image = image {
strongSelf.contents = image.cgImage
strongSelf.isDisplayingPlaceholder = true
}
if isFinal {
strongSelf.loadAnimation()
}
}
}
} else {
guard let strongSelf = self else {
return
}
let _ = isThumbnailCancelled.swap(true)
strongSelf.loadAnimation()
}
})
}
}
private func loadAnimation() {
/*guard let arguments = self.arguments else {
return
}
guard let file = self.file else {
return
}
let isTemplate = file.isCustomTemplateEmoji
let context = arguments.context
if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji {
let keyframeOnly = arguments.pixelSize.width >= 120.0
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil))
} else {
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: { options in
let dataDisposable = context.postbox.mediaBox.resourceData(file.resource).start(next: { result in
guard result.complete else {
return
}
cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: isTemplate ? .white : nil)
})
let fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: context.postbox, userLocation: arguments.userLocation, fileReference: .customEmoji(media: file), resource: file.resource).start()
return ActionDisposable {
dataDisposable.dispose()
fetchDisposable.dispose()
}
})
}*/
}
override public func updateDisplayPlaceholder(displayPlaceholder: Bool) {
if self.isDisplayingPlaceholder == displayPlaceholder {
return
}
self.isDisplayingPlaceholder = displayPlaceholder
}
override public func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
if self.isDisplayingPlaceholder {
self.isDisplayingPlaceholder = false
if let current = self.contents {
let previousLayer = SimpleLayer()
previousLayer.contents = current
previousLayer.frame = self.frame
self.superlayer?.insertSublayer(previousLayer, below: self)
previousLayer.opacity = 0.0
previousLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak previousLayer] _ in
previousLayer?.removeFromSuperlayer()
})
self.contents = contents
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
} else {
self.contents = contents
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
self.contents = contents
}
}
}