Swiftgram/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift
2022-10-25 19:58:51 +03:00

364 lines
14 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
enum MediaPickerGridItemContent: Equatable {
case asset(PHFetchResult<PHAsset>, Int)
case media(MediaPickerScreen.Subject.Media, Int)
}
final class MediaPickerGridItem: GridItem {
let content: MediaPickerGridItemContent
let interaction: MediaPickerInteraction
let theme: PresentationTheme
let section: GridSection? = nil
init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme) {
self.content = content
self.interaction = interaction
self.theme = theme
}
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)
return node
case let .media(media, index):
let node = MediaPickerGridItemNode()
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme)
return node
}
}
func update(node: GridItemNode) {
switch self.content {
case let .asset(fetchResult, index):
guard let node = node as? MediaPickerGridItemNode else {
assertionFailure()
return
}
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme)
case let .media(media, index):
guard let node = node as? MediaPickerGridItemNode else {
assertionFailure()
return
}
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme)
}
}
}
private let maskImage = generateImage(CGSize(width: 1.0, height: 24.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.6).cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
final class MediaPickerGridItemNode: GridItemNode {
var currentMediaState: (TGMediaSelectableItem, Int)?
var currentState: (PHFetchResult<PHAsset>, Int)?
private let imageNode: ImageNode
private var checkNode: InteractiveCheckNode?
private let gradientNode: ASImageNode
private let typeIconNode: ASImageNode
private let durationNode: ImmediateTextNode
private var interaction: MediaPickerInteraction?
private var theme: PresentationTheme?
private var currentIsPreviewing = false
var selected: (() -> Void)?
override init() {
self.imageNode = ImageNode()
self.imageNode.clipsToBounds = true
self.imageNode.contentMode = .scaleAspectFill
self.imageNode.isLayerBacked = false
self.imageNode.animateFirstTransition = false
self.gradientNode = ASImageNode()
self.gradientNode.displaysAsynchronously = false
self.gradientNode.displayWithoutProcessing = true
self.gradientNode.image = maskImage
self.typeIconNode = ASImageNode()
self.typeIconNode.displaysAsynchronously = false
self.typeIconNode.displayWithoutProcessing = true
self.durationNode = ImmediateTextNode()
super.init()
self.addSubnode(self.imageNode)
}
var identifier: String {
return self.selectableItem?.uniqueIdentifier ?? ""
}
var selectableItem: TGMediaSelectableItem? {
if let (media, _) = self.currentMediaState {
return media
} else if let (fetchResult, index) = self.currentState {
return TGMediaAsset(phAsset: fetchResult[index])
} else {
return nil
}
}
var _cachedTag: Int32?
var tag: Int32? {
// if let tag = self._cachedTag {
// return tag
// } else if let asset = self.asset, let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
// let tag = Month(localTimestamp: Int32(localTimestamp)).packedValue
// self._cachedTag = tag
// return tag
// } else {
return nil
// }
}
func updateSelectionState(animated: Bool = false) {
if self.checkNode == nil, let _ = self.interaction?.selectionState, 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 {
interaction.toggleSelection(selectableItem, value, false)
}
}
self.addSubnode(checkNode)
self.checkNode = checkNode
self.setNeedsLayout()
}
if let interaction = self.interaction, let selectionState = interaction.selectionState {
let selected = selectionState.isIdentifierSelected(self.identifier)
if let selectableItem = self.selectableItem {
let index = selectionState.index(of: selectableItem)
if index != NSNotFound {
self.checkNode?.content = .counter(Int(index))
}
}
self.checkNode?.setSelected(selected, animated: animated)
}
}
func updateHiddenMedia() {
let wasHidden = self.isHidden
self.isHidden = self.interaction?.hiddenMediaId == self.identifier
if !self.isHidden && wasHidden {
self.animateFadeIn(animateCheckNode: true)
}
}
func animateFadeIn(animateCheckNode: Bool) {
if animateCheckNode {
self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme) {
self.interaction = interaction
self.theme = theme
self.backgroundColor = theme.list.mediaPlaceholderColor
if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentState!.1 != index {
// let editingContext = interaction.editingState
// let asset = media.asset as? TGMediaEditableItem
//
// let editedSignal = Signal<UIImage?, NoError> { subscriber in
// if let signal = editingContext.thumbnailImageSignal(forIdentifier: media.identifier) {
// 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 originalImageSignal = Signal<UIImage?, NoError> { subscriber in
// if let signal = asset?.thumbnailImageSignal?()
// }
//
// let scale = min(2.0, UIScreenScale)
// let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
// let originalSignal: Signal<UIImage, NoError> = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false)
// let imageSignal: Signal<UIImage?, NoError> = editedSignal
// |> mapToSignal { result in
// if let result = result {
// return .single(result)
// } else {
// return originalSignal
// }
// }
// self.imageNode.setSignal(imageSignal)
//
// if case .video = media, let asset = media.asset as? TGCameraCapturedVideo {
// self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo")
//
// if self.typeIconNode.supernode == nil {
// self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(asset.videoDuration)), font: Font.semibold(12.0), textColor: .white)
//
// self.addSubnode(self.gradientNode)
// self.addSubnode(self.typeIconNode)
// self.addSubnode(self.durationNode)
// self.setNeedsLayout()
// }
// }
//
self.currentMediaState = (media.asset, index)
self.setNeedsLayout()
}
self.updateSelectionState()
self.updateHiddenMedia()
}
func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult<PHAsset>, index: Int, theme: PresentationTheme) {
self.interaction = interaction
self.theme = theme
self.backgroundColor = theme.list.mediaPlaceholderColor
if self.currentState == nil || self.currentState!.0 !== fetchResult || self.currentState!.1 != index {
let editingContext = interaction.editingState
let asset = fetchResult.object(at: index)
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(width: 128.0 * scale, height: 128.0 * scale)
let originalSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false)
let imageSignal: Signal<UIImage?, NoError> = editedSignal
|> mapToSignal { result in
if let result = result {
return .single(result)
} else {
return originalSignal
}
}
self.imageNode.setSignal(imageSignal)
if asset.mediaType == .video {
if asset.mediaSubtypes.contains(.videoHighFrameRate) {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaSlomo")
} else if asset.mediaSubtypes.contains(.videoTimelapse) {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaTimelapse")
} else {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo")
}
if self.typeIconNode.supernode == nil {
self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(asset.duration)), font: Font.semibold(12.0), textColor: .white)
self.addSubnode(self.gradientNode)
self.addSubnode(self.typeIconNode)
self.addSubnode(self.durationNode)
self.setNeedsLayout()
}
} else {
if self.typeIconNode.supernode != nil {
self.gradientNode.removeFromSupernode()
self.typeIconNode.removeFromSupernode()
self.durationNode.removeFromSupernode()
}
}
self.currentState = (fetchResult, index)
self.setNeedsLayout()
}
self.updateSelectionState()
self.updateHiddenMedia()
}
override func layout() {
super.layout()
self.imageNode.frame = self.bounds
self.gradientNode.frame = CGRect(x: 0.0, y: self.bounds.height - 24.0, width: self.bounds.width, height: 24.0)
self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0)
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 - 7.0, y: self.bounds.height - durationSize.height - 5.0), size: durationSize)
}
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)
}
func transitionView() -> UIView {
let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)!
view.frame = self.convert(self.bounds, to: nil)
return view
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
guard let (fetchResult, index) = self.currentState else {
return
}
self.interaction?.openMedia(fetchResult, index, self.imageNode.image)
}
}