Swiftgram/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift
Ilya Laktyushin 5659120fa9 Various fixes
2024-06-28 01:59:26 +04:00

867 lines
36 KiB
Swift

import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import AccountContext
import TelegramPresentationData
import TelegramStringFormatting
import Photos
import CheckNode
import LegacyComponents
import PhotoResources
import InvisibleInkDustNode
import ImageBlur
import FastBlur
import MediaEditor
import RadialStatusNode
private let leftShadowImage: UIImage = {
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
let image = generateImage(baseImage.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: -1.0, y: 1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
UIGraphicsPushContext(context)
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
UIGraphicsPopContext()
})
return image!
}()
private let rightShadowImage: UIImage = {
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
let image = generateImage(baseImage.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
UIGraphicsPopContext()
})
return image!
}()
enum MediaPickerGridItemContent: Equatable {
case asset(PHFetchResult<PHAsset>, Int)
case media(MediaPickerScreen.Subject.Media, Int)
case draft(MediaEditorDraft, Int)
}
final class MediaPickerGridItem: GridItem {
let content: MediaPickerGridItemContent
let interaction: MediaPickerInteraction
let theme: PresentationTheme
let strings: PresentationStrings
let selectable: Bool
let enableAnimations: Bool
let stories: Bool
let section: GridSection? = nil
init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, strings: PresentationStrings, selectable: Bool, enableAnimations: Bool, stories: Bool) {
self.content = content
self.interaction = interaction
self.strings = strings
self.theme = theme
self.selectable = selectable
self.enableAnimations = enableAnimations
self.stories = stories
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
switch self.content {
case let .asset(fetchResult, index):
let node = MediaPickerGridItemNode()
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
return node
case let .media(media, index):
let node = MediaPickerGridItemNode()
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
return node
case let .draft(draft, index):
let node = MediaPickerGridItemNode()
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, strings: self.strings, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
return node
}
}
func update(node: GridItemNode) {
guard let node = node as? MediaPickerGridItemNode else {
assertionFailure()
return
}
switch self.content {
case let .asset(fetchResult, index):
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
case let .media(media, index):
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
case let .draft(draft, index):
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, strings: self.strings, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
}
}
}
final class MediaPickerGridItemNode: GridItemNode {
var currentMediaState: (TGMediaSelectableItem, Int)?
var currentAssetState: (PHFetchResult<PHAsset>, Int)?
var currentAsset: PHAsset?
var currentDraftState: (MediaEditorDraft, Int)?
var enableAnimations: Bool = true
var stories: Bool = false
private var selectable: Bool = false
private let backgroundNode: ASImageNode
private let imageNode: ImageNode
private var checkNode: InteractiveCheckNode?
private let leftShadowNode: ASImageNode
private let rightShadowNode: ASImageNode
private let typeIconNode: ASImageNode
private let durationNode: ImmediateTextNode
private let draftNode: ImmediateTextNode
private var statusNode: RadialStatusNode?
private let activateAreaNode: AccessibilityAreaNode
private var interaction: MediaPickerInteraction?
private var theme: PresentationTheme?
private struct SelectionState: Equatable {
let selected: Bool
let index: Int?
let count: Int
}
private let selectionPromise = ValuePromise<SelectionState>(SelectionState(selected: false, index: nil, count: 0))
private let spoilerDisposable = MetaDisposable()
var spoilerNode: SpoilerOverlayNode?
var priceNode: PriceNode?
private let progressDisposable = MetaDisposable()
private var currentIsPreviewing = false
var selected: (() -> Void)?
override init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.contentMode = .scaleToFill
self.backgroundNode.isLayerBacked = true
self.imageNode = ImageNode()
self.imageNode.clipsToBounds = true
self.imageNode.contentMode = .scaleAspectFill
self.imageNode.isLayerBacked = true
self.imageNode.animateFirstTransition = false
self.leftShadowNode = ASImageNode()
self.leftShadowNode.displaysAsynchronously = false
self.leftShadowNode.displayWithoutProcessing = true
self.leftShadowNode.image = leftShadowImage
self.leftShadowNode.isLayerBacked = true
self.rightShadowNode = ASImageNode()
self.rightShadowNode.displaysAsynchronously = false
self.rightShadowNode.displayWithoutProcessing = true
self.rightShadowNode.image = rightShadowImage
self.rightShadowNode.isLayerBacked = true
self.typeIconNode = ASImageNode()
self.typeIconNode.displaysAsynchronously = false
self.typeIconNode.displayWithoutProcessing = true
self.typeIconNode.isLayerBacked = true
self.durationNode = ImmediateTextNode()
self.durationNode.isLayerBacked = true
self.durationNode.textShadowColor = UIColor(white: 0.0, alpha: 0.4)
self.durationNode.textShadowBlur = 4.0
self.draftNode = ImmediateTextNode()
self.activateAreaNode = AccessibilityAreaNode()
self.activateAreaNode.accessibilityTraits = [.image]
super.init()
self.clipsToBounds = true
self.addSubnode(self.imageNode)
self.addSubnode(self.activateAreaNode)
self.imageNode.contentUpdated = { [weak self] image in
self?.spoilerNode?.setImage(image)
}
}
deinit {
self.spoilerDisposable.dispose()
}
var identifier: String {
if let (draft, _) = self.currentDraftState {
return draft.path
} else {
return self.selectableItem?.uniqueIdentifier ?? ""
}
}
var selectableItem: TGMediaSelectableItem? {
if let (media, _) = self.currentMediaState {
return media
} else if let (fetchResult, index) = self.currentAssetState {
return TGMediaAsset(phAsset: fetchResult[index])
} else {
return nil
}
}
var _cachedTag: Int32?
var tag: Int32? {
if let tag = self._cachedTag {
return tag
} else if let (fetchResult, index) = self.currentAssetState {
let asset = fetchResult.object(at: index)
if let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
let tag = Month(localTimestamp: Int32(exactly: floor(localTimestamp)) ?? 0).packedValue
self._cachedTag = tag
return tag
} else {
return nil
}
} else if let (draft, _) = self.currentDraftState {
let tag = Month(localTimestamp: draft.timestamp).packedValue
self._cachedTag = tag
return tag
} else {
return nil
}
}
func updateSelectionState(animated: Bool = false) {
if self.checkNode == nil, let _ = self.interaction?.selectionState, self.selectable, let theme = self.theme {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay))
checkNode.valueChanged = { [weak self] value in
if let strongSelf = self, let interaction = strongSelf.interaction, let selectableItem = strongSelf.selectableItem {
if !interaction.toggleSelection(selectableItem, value, false) {
strongSelf.checkNode?.setSelected(false, animated: false)
}
}
}
self.addSubnode(checkNode)
self.checkNode = checkNode
self.setNeedsLayout()
}
if let interaction = self.interaction, let selectionState = interaction.selectionState {
let selected = selectionState.isIdentifierSelected(self.identifier)
var selectionIndex: Int?
if let selectableItem = self.selectableItem {
let index = selectionState.index(of: selectableItem)
if index != NSNotFound {
self.checkNode?.content = .counter(Int(index))
selectionIndex = Int(index)
}
}
self.checkNode?.setSelected(selected, animated: animated)
self.selectionPromise.set(SelectionState(selected: selected, index: selectionIndex, count: selectionState.selectedItems().count))
}
}
private var innerIsHidden = false
func updateHiddenMedia() {
let wasHidden = self.innerIsHidden
if self.identifier == self.interaction?.hiddenMediaId {
self.isHidden = true
self.innerIsHidden = true
} else {
self.isHidden = false
self.innerIsHidden = false
if wasHidden {
self.animateFadeIn(animateCheckNode: true, animateSpoilerNode: true)
}
}
}
func animateFadeIn(animateCheckNode: Bool, animateSpoilerNode: Bool) {
if animateCheckNode {
self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
self.leftShadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.rightShadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.draftNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.priceNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if animateSpoilerNode || self.priceNode != nil {
self.spoilerNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
func updateProgress(_ value: Float?, animated: Bool) {
if let value {
let statusNode: RadialStatusNode
if let current = self.statusNode {
statusNode = current
} else {
statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6))
statusNode.isUserInteractionEnabled = false
self.addSubnode(statusNode)
self.statusNode = statusNode
}
let adjustedProgress = max(0.027, CGFloat(value))
let state: RadialStatusNodeState = .progress(color: .white, lineWidth: nil, value: adjustedProgress, cancelEnabled: true, animateRotation: true)
statusNode.transitionToState(state)
} else if let statusNode = self.statusNode {
self.statusNode = nil
if animated {
statusNode.transitionToState(.none, animated: true, completion: { [weak statusNode] in
statusNode?.removeFromSupernode()
})
} else {
statusNode.removeFromSupernode()
}
}
}
func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, strings: PresentationStrings, selectable: Bool, enableAnimations: Bool, stories: Bool) {
self.interaction = interaction
self.theme = theme
self.selectable = selectable
self.enableAnimations = enableAnimations
self.backgroundColor = theme.list.mediaPlaceholderColor
if self.currentDraftState == nil || self.currentDraftState?.0.path != draft.path || self.currentDraftState!.1 != index || self.currentAssetState != nil {
let imageSignal: Signal<UIImage?, NoError> = .single(draft.thumbnail)
self.imageNode.setSignal(imageSignal)
self.currentDraftState = (draft, index)
if self.currentAssetState != nil {
self.currentAsset = nil
self.currentAssetState = nil
self.typeIconNode.removeFromSupernode()
self.progressDisposable.set(nil)
self.updateProgress(nil, animated: false)
self.backgroundNode.image = nil
self.imageNode.contentMode = .scaleAspectFill
}
if self.draftNode.supernode == nil {
self.draftNode.attributedText = NSAttributedString(string: strings.MediaEditor_Draft, font: Font.semibold(12.0), textColor: .white)
self.addSubnode(self.draftNode)
}
if draft.isVideo {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo")
self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(draft.duration ?? 0.0)), font: Font.semibold(11.0), textColor: .white)
if self.typeIconNode.supernode == nil {
self.addSubnode(self.rightShadowNode)
self.addSubnode(self.typeIconNode)
self.addSubnode(self.durationNode)
self.setNeedsLayout()
}
} else {
if self.typeIconNode.supernode != nil {
self.typeIconNode.removeFromSupernode()
}
if self.durationNode.supernode != nil {
self.durationNode.removeFromSupernode()
}
if self.leftShadowNode.supernode != nil {
self.leftShadowNode.removeFromSupernode()
}
if self.rightShadowNode.supernode != nil {
self.rightShadowNode.removeFromSupernode()
}
}
self.setNeedsLayout()
}
self.updateSelectionState()
self.updateHiddenMedia()
}
func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
self.interaction = interaction
self.theme = theme
self.selectable = selectable
self.enableAnimations = enableAnimations
self.stories = stories
self.backgroundColor = theme.list.mediaPlaceholderColor
if stories {
if self.backgroundNode.supernode == nil {
self.insertSubnode(self.backgroundNode, at: 0)
}
}
if self.draftNode.supernode != nil {
self.draftNode.removeFromSupernode()
}
if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentMediaState!.1 != index {
self.currentMediaState = (media.asset, index)
if self.draftNode.supernode != nil {
self.draftNode.removeFromSupernode()
}
self.setNeedsLayout()
}
self.updateSelectionState()
self.updateHiddenMedia()
}
func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult<PHAsset>, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
self.interaction = interaction
self.theme = theme
self.selectable = selectable
self.enableAnimations = enableAnimations
self.stories = stories
self.backgroundColor = theme.list.mediaPlaceholderColor
if stories {
if self.backgroundNode.supernode == nil {
self.insertSubnode(self.backgroundNode, at: 0)
}
}
if self.draftNode.supernode != nil {
self.draftNode.removeFromSupernode()
}
if self.currentAssetState == nil || self.currentAssetState!.0 !== fetchResult || self.currentAssetState!.1 != index || self.currentDraftState != nil {
let editingContext = interaction.editingState
let asset = fetchResult.object(at: index)
if asset.localIdentifier == self.currentAsset?.localIdentifier {
return
}
self.backgroundNode.image = nil
self.progressDisposable.set(
(interaction.downloadManager.downloadProgress(identifier: asset.localIdentifier)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
switch status {
case .none, .completed:
self.updateProgress(nil, animated: true)
case let .progress(progress):
self.updateProgress(progress, animated: true)
}
}
})
)
self.backgroundNode.image = nil
if #available(iOS 15.0, *) {
self.activateAreaNode.accessibilityLabel = "Photo \(asset.creationDate?.formatted(date: .abbreviated, time: .standard) ?? "")"
}
let editedSignal = Signal<UIImage?, NoError> { subscriber in
if let signal = editingContext.thumbnailImageSignal(forIdentifier: asset.localIdentifier) {
let disposable = signal.start(next: { next in
if let image = next as? UIImage {
subscriber.putNext(image)
} else {
subscriber.putNext(nil)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
let scale = min(2.0, UIScreenScale)
let targetSize: CGSize
if stories {
targetSize = CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale)
} else {
targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
}
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .opportunistic, synchronous: false)
// |> then(
// assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
// |> delay(0.03, queue: Queue.concurrentDefaultQueue())
// )
if stories {
self.imageNode.contentUpdated = { [weak self] image in
if let self {
if self.backgroundNode.image == nil {
if let image, image.size.width > image.size.height {
self.imageNode.contentMode = .scaleAspectFit
Queue.concurrentDefaultQueue().async {
let colors = mediaEditorGetGradientColors(from: image)
let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: colors.array)
Queue.mainQueue().async {
self.backgroundNode.image = gradientImage
}
}
} else {
self.imageNode.contentMode = .scaleAspectFill
}
}
}
}
}
let originalSignal = assetImageSignal
let imageSignal: Signal<UIImage?, NoError> = editedSignal
|> mapToSignal { result in
if let result = result {
return .single(result)
} else {
return originalSignal
}
}
self.imageNode.setSignal(imageSignal)
let spoilerSignal = Signal<Bool, NoError> { subscriber in
if let signal = editingContext.spoilerSignal(forIdentifier: asset.localIdentifier) {
let disposable = signal.start(next: { next in
if let next = next as? Bool {
subscriber.putNext(next)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
let priceSignal = Signal<Int64?, NoError> { subscriber in
if let signal = editingContext.priceSignal(forIdentifier: asset.localIdentifier) {
let disposable = signal.start(next: { next in
subscriber.putNext(next as? Int64)
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal, self.selectionPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price, selectionState in
guard let strongSelf = self else {
return
}
strongSelf.updateHasSpoiler(hasSpoiler, price: selectionState.selected ? price : nil, isSingle: selectionState.count == 1 || selectionState.index == 1)
}))
if self.currentDraftState != nil {
self.currentDraftState = nil
}
var typeIcon: UIImage?
var duration: String?
if asset.isFavorite {
typeIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Favorite"), color: .white)
} else if asset.mediaType == .video {
if asset.mediaSubtypes.contains(.videoHighFrameRate) {
typeIcon = UIImage(bundleImageName: "Media Editor/MediaSlomo")
} else if asset.mediaSubtypes.contains(.videoTimelapse) {
typeIcon = UIImage(bundleImageName: "Media Editor/MediaTimelapse")
} else {
typeIcon = UIImage(bundleImageName: "Media Editor/MediaVideo")
}
duration = stringForDuration(Int32(asset.duration))
}
if typeIcon != nil {
if self.leftShadowNode.supernode == nil {
self.addSubnode(self.leftShadowNode)
}
} else if self.leftShadowNode.supernode != nil {
self.leftShadowNode.removeFromSupernode()
}
if duration != nil {
if self.rightShadowNode.supernode == nil {
self.addSubnode(self.rightShadowNode)
}
} else if self.rightShadowNode.supernode != nil {
self.rightShadowNode.removeFromSupernode()
}
if let typeIcon {
self.typeIconNode.image = typeIcon
if self.typeIconNode.supernode == nil {
self.addSubnode(self.typeIconNode)
}
} else if self.typeIconNode.supernode != nil {
self.typeIconNode.removeFromSupernode()
}
if let duration {
self.durationNode.attributedText = NSAttributedString(string: duration, font: Font.semibold(11.0), textColor: .white)
if self.durationNode.supernode == nil {
self.addSubnode(self.durationNode)
}
} else if self.durationNode.supernode != nil {
self.durationNode.removeFromSupernode()
}
self.currentAssetState = (fetchResult, index)
self.currentAsset = asset
self.setNeedsLayout()
}
self.updateSelectionState()
self.updateHiddenMedia()
}
private var currentPrice: Int64?
private var didSetupSpoiler = false
private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?, isSingle: Bool) {
var animated = true
if !self.didSetupSpoiler {
animated = false
self.didSetupSpoiler = true
}
self.currentPrice = isSingle ? price : nil
if hasSpoiler || price != nil {
if self.spoilerNode == nil {
let spoilerNode = SpoilerOverlayNode(enableAnimations: self.enableAnimations)
self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode)
self.spoilerNode = spoilerNode
spoilerNode.setImage(self.imageNode.image)
if animated {
spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let bounds = self.bounds
self.spoilerNode?.update(size: bounds.size, transition: .immediate)
self.spoilerNode?.frame = CGRect(origin: .zero, size: bounds.size)
if let price {
let priceNode: PriceNode
if let currentPriceNode = self.priceNode {
priceNode = currentPriceNode
} else {
priceNode = PriceNode()
if let spoilerNode = self.spoilerNode {
self.insertSubnode(priceNode, aboveSubnode: spoilerNode)
}
self.priceNode = priceNode
if animated {
priceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
self.priceNode?.update(size: bounds.size, price: isSingle ? price : nil, small: true, transition: .immediate)
}
} else if let spoilerNode = self.spoilerNode {
self.spoilerNode = nil
spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in
spoilerNode?.removeFromSupernode()
})
if let priceNode = self.priceNode {
self.priceNode = nil
priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak priceNode] _ in
priceNode?.removeFromSupernode()
})
}
}
}
override func layout() {
super.layout()
let backgroundSize = CGSize(width: self.bounds.width, height: floorToScreenPixels(self.bounds.height / 9.0 * 16.0))
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((self.bounds.height - backgroundSize.height) / 2.0)), size: backgroundSize)
self.imageNode.frame = self.bounds
self.leftShadowNode.frame = CGRect(x: 0.0, y: self.bounds.height - leftShadowImage.size.height, width: min(leftShadowImage.size.width, self.bounds.width), height: leftShadowImage.size.height)
self.rightShadowNode.frame = CGRect(x: self.bounds.width - min(rightShadowImage.size.width, self.bounds.width), y: self.bounds.height - rightShadowImage.size.height, width: min(rightShadowImage.size.width, self.bounds.width), height: rightShadowImage.size.height)
self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0)
self.activateAreaNode.frame = self.bounds
if self.durationNode.supernode != nil {
let durationSize = self.durationNode.updateLayout(self.bounds.size)
self.durationNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - durationSize.width - 6.0, y: self.bounds.height - durationSize.height - 6.0), size: durationSize)
}
if self.draftNode.supernode != nil {
let draftSize = self.draftNode.updateLayout(self.bounds.size)
self.draftNode.frame = CGRect(origin: CGPoint(x: 7.0, y: 5.0), size: draftSize)
}
let checkSize = CGSize(width: 29.0, height: 29.0)
self.checkNode?.frame = CGRect(origin: CGPoint(x: self.bounds.width - checkSize.width - 3.0, y: 3.0), size: checkSize)
if let spoilerNode = self.spoilerNode, self.bounds.width > 0.0 {
spoilerNode.frame = self.bounds
spoilerNode.update(size: self.bounds.size, transition: .immediate)
}
if let priceNode = self.priceNode, self.bounds.width > 0.0 {
priceNode.frame = self.bounds
priceNode.update(size: self.bounds.size, price: self.currentPrice, small: true, transition: .immediate)
}
let statusSize = CGSize(width: 40.0, height: 40.0)
if let statusNode = self.statusNode {
statusNode.view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.bounds.width - statusSize.width) / 2.0), y: floorToScreenPixels((self.bounds.height - statusSize.height) / 2.0)), size: statusSize)
}
}
func transitionView(snapshot: Bool) -> UIView {
if snapshot {
let view = self.imageNode.layer.snapshotContentTreeAsView(unhide: true)!
view.frame = self.convert(self.bounds, to: nil)
return view
} else {
return self.view
}
}
func transitionImage() -> UIImage? {
if let backgroundImage = self.backgroundNode.image {
let size = CGSize(width: self.bounds.width, height: self.bounds.height / 9.0 * 16.0)
return generateImage(size, contextGenerator: { size, context in
if let cgImage = backgroundImage.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
if let image = self.imageNode.image, let cgImage = image.cgImage {
let fittedSize = image.size.fitted(size)
let fittedFrame = CGRect(origin: CGPoint(x: (size.width - fittedSize.width) / 2.0, y: (size.height - fittedSize.height) / 2.0), size: fittedSize)
context.draw(cgImage, in: fittedFrame)
}
}
})
} else {
return self.imageNode.image
}
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
if let (draft, _) = self.currentDraftState {
self.interaction?.openDraft(draft, self.imageNode.image)
return
}
guard let (fetchResult, index) = self.currentAssetState else {
return
}
if self.statusNode != nil {
if let asset = self.currentAsset {
self.interaction?.downloadManager.cancel(identifier: asset.localIdentifier)
}
} else {
self.interaction?.openMedia(fetchResult, index, self.imageNode.image)
}
}
}
class SpoilerOverlayNode: ASDisplayNode {
private let blurNode: ASImageNode
let dustNode: MediaDustNode
private var maskView: UIView?
private var maskLayer: CAShapeLayer?
init(enableAnimations: Bool) {
self.blurNode = ASImageNode()
self.blurNode.displaysAsynchronously = false
self.blurNode.contentMode = .scaleAspectFill
self.dustNode = MediaDustNode(enableAnimations: enableAnimations)
super.init()
self.clipsToBounds = true
self.isUserInteractionEnabled = false
self.addSubnode(self.blurNode)
self.addSubnode(self.dustNode)
}
override func didLoad() {
super.didLoad()
let maskView = UIView()
self.maskView = maskView
// self.dustNode.view.mask = maskView
let maskLayer = CAShapeLayer()
maskLayer.fillRule = .evenOdd
maskLayer.fillColor = UIColor.white.cgColor
maskView.layer.addSublayer(maskLayer)
self.maskLayer = maskLayer
}
func setImage(_ image: UIImage?) {
self.blurNode.image = image.flatMap { blurredImage($0) }
}
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.blurNode, frame: CGRect(origin: .zero, size: size))
transition.updateFrame(node: self.dustNode, frame: CGRect(origin: .zero, size: size))
self.dustNode.update(size: size, color: .white, transition: transition)
}
}
private func blurredImage(_ image: UIImage) -> UIImage? {
guard let image = image.cgImage else {
return nil
}
let thumbnailSize = CGSize(width: image.width, height: image.height)
let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0))
if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) {
thumbnailContext.withFlippedContext { c in
c.interpolationQuality = .none
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize))
}
imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes)
let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0))
if let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) {
thumbnailContext2.withFlippedContext { c in
c.interpolationQuality = .none
if let image = thumbnailContext.generateImage()?.cgImage {
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size))
}
}
imageFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes)
adjustSaturationInContext(context: thumbnailContext2, saturation: 1.7)
return thumbnailContext2.generateImage()
}
}
return nil
}