Files
ghostgram/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchGlobaltem.swift

558 lines
24 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import ListSectionHeaderNode
import AccountContext
import StickerResources
final class StickerPaneSearchGlobalSection: GridSection {
let title: String?
let theme: PresentationTheme
var height: CGFloat {
if let _ = self.title {
return 28.0
} else {
return 0.0
}
}
var hashValue: Int {
if let _ = self.title {
return 1
} else {
return 0
}
}
init(title: String?, theme: PresentationTheme) {
self.title = title
self.theme = theme
}
func isEqual(to: GridSection) -> Bool {
if let to = to as? StickerPaneSearchGlobalSection {
return to.hashValue == self.hashValue
} else {
return false
}
}
func node() -> ASDisplayNode {
return StickerPaneSearchGlobalSectionNode(theme: self.theme, title: self.title ?? "")
}
}
private final class StickerPaneSearchGlobalSectionNode: ASDisplayNode {
private let node: ListSectionHeaderNode
init(theme: PresentationTheme, title: String) {
self.node = ListSectionHeaderNode(theme: theme)
super.init()
if !title.isEmpty {
self.node.title = title
self.addSubnode(self.node)
}
}
override func layout() {
super.layout()
self.node.frame = self.bounds
self.node.updateLayout(size: self.bounds.size, leftInset: 0.0, rightInset: 0.0)
}
}
public final class StickerPaneSearchGlobalItemContext {
public var canPlayMedia: Bool
public init(canPlayMedia: Bool = false) {
self.canPlayMedia = canPlayMedia
}
}
public final class StickerPaneSearchGlobalItem: GridItem {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let listAppearance: Bool
public let fillsRow: Bool
public let info: StickerPackCollectionInfo.Accessor
public let topItems: [StickerPackItem]
public let topSeparator: Bool
public let regularInsets: Bool
public let installed: Bool
public let installing: Bool
public let unread: Bool
public let open: () -> Void
public let install: () -> Void
public let getItemIsPreviewed: (StickerPackItem) -> Bool
public let itemContext: StickerPaneSearchGlobalItemContext
public let section: GridSection?
public var fillsRowWithHeight: (CGFloat, Bool)? {
var additionalHeight: CGFloat = 0.0
if self.regularInsets {
additionalHeight = 12.0 + 12.0
} else {
additionalHeight += 12.0
if self.topSeparator {
additionalHeight += 12.0
}
}
return (128.0 + additionalHeight, self.fillsRow)
}
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, fillsRow: Bool = true, info: StickerPackCollectionInfo.Accessor, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) {
self.context = context
self.theme = theme
self.strings = strings
self.listAppearance = listAppearance
self.fillsRow = fillsRow
self.info = info
self.topItems = topItems
self.topSeparator = topSeparator
self.regularInsets = regularInsets
self.installed = installed
self.installing = installing
self.unread = unread
self.open = open
self.install = install
self.getItemIsPreviewed = getItemIsPreviewed
self.itemContext = itemContext
self.section = StickerPaneSearchGlobalSection(title: sectionTitle, theme: theme)
}
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = StickerPaneSearchGlobalItemNode()
node.setup(item: self)
return node
}
public func update(node: GridItemNode) {
guard let node = node as? StickerPaneSearchGlobalItemNode else {
assertionFailure()
return
}
node.setup(item: self)
}
}
private let titleFont = Font.bold(16.0)
private let statusFont = Font.regular(15.0)
private let buttonFont = Font.semibold(13.0)
public class StickerPaneSearchGlobalItemNode: GridItemNode {
private let titleNode: TextNode
private let descriptionNode: TextNode
private let unreadNode: ASImageNode
private let installTextNode: TextNode
private let installBackgroundNode: ASImageNode
private let installButtonNode: HighlightTrackingButtonNode
private let uninstallTextNode: TextNode
private let uninstallBackgroundNode: ASImageNode
private let uninstallButtonNode: HighlightTrackingButtonNode
private var itemNodes: [TrendingTopItemNode]
private let topSeparatorNode: ASDisplayNode
private var highlightNode: ASDisplayNode?
public var item: StickerPaneSearchGlobalItem?
private var appliedItem: StickerPaneSearchGlobalItem?
private let preloadDisposable = MetaDisposable()
private let preloadedStickerPackThumbnailDisposable = MetaDisposable()
private var preloadedThumbnail = false
private var canPlay = false
private var canPlayMedia: Bool = false {
didSet {
if self.canPlayMedia != oldValue {
self.updatePlayback()
}
}
}
public override var isVisibleInGrid: Bool {
didSet {
if oldValue != self.isVisibleInGrid {
self.updatePlayback()
}
}
}
private func updatePlayback() {
let canPlay = self.canPlayMedia && self.isVisibleInGrid
if canPlay != self.canPlay {
self.canPlay = canPlay
for node in self.itemNodes {
node.visibility = self.canPlay
}
if let item = self.item, self.isVisibleInGrid, !self.preloadedThumbnail {
self.preloadedThumbnail = true
self.preloadedStickerPackThumbnailDisposable.set(preloadedStickerPackThumbnail(account: item.context.account, info: item.info, items: item.topItems).start())
}
}
}
public override init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.descriptionNode = TextNode()
self.descriptionNode.isUserInteractionEnabled = false
self.descriptionNode.contentMode = .left
self.descriptionNode.contentsScale = UIScreen.main.scale
self.unreadNode = ASImageNode()
self.unreadNode.isLayerBacked = true
self.unreadNode.displayWithoutProcessing = true
self.unreadNode.displaysAsynchronously = false
self.installTextNode = TextNode()
self.installTextNode.isUserInteractionEnabled = false
self.installTextNode.contentMode = .left
self.installTextNode.contentsScale = UIScreen.main.scale
self.installBackgroundNode = ASImageNode()
self.installBackgroundNode.isLayerBacked = true
self.installBackgroundNode.displayWithoutProcessing = true
self.installBackgroundNode.displaysAsynchronously = false
self.installButtonNode = HighlightTrackingButtonNode()
self.uninstallTextNode = TextNode()
self.uninstallTextNode.isUserInteractionEnabled = false
self.uninstallTextNode.contentMode = .left
self.uninstallTextNode.contentsScale = UIScreen.main.scale
self.uninstallBackgroundNode = ASImageNode()
self.uninstallBackgroundNode.isLayerBacked = true
self.uninstallBackgroundNode.displayWithoutProcessing = true
self.uninstallBackgroundNode.displaysAsynchronously = false
self.uninstallButtonNode = HighlightTrackingButtonNode()
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
self.itemNodes = []
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.descriptionNode)
self.addSubnode(self.unreadNode)
self.addSubnode(self.installBackgroundNode)
self.addSubnode(self.installTextNode)
self.addSubnode(self.installButtonNode)
self.addSubnode(self.uninstallBackgroundNode)
self.addSubnode(self.uninstallTextNode)
self.addSubnode(self.uninstallButtonNode)
self.addSubnode(self.topSeparatorNode)
self.installButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.installBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.installBackgroundNode.alpha = 0.4
strongSelf.installTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.installTextNode.alpha = 0.4
} else {
strongSelf.installBackgroundNode.alpha = 1.0
strongSelf.installBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.installTextNode.alpha = 1.0
strongSelf.installTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.installButtonNode.addTarget(self, action: #selector(self.installPressed), forControlEvents: .touchUpInside)
self.uninstallButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.uninstallBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.uninstallBackgroundNode.alpha = 0.4
strongSelf.uninstallTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.uninstallTextNode.alpha = 0.4
} else {
strongSelf.uninstallBackgroundNode.alpha = 1.0
strongSelf.uninstallBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.uninstallTextNode.alpha = 1.0
strongSelf.uninstallTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.uninstallButtonNode.addTarget(self, action: #selector(self.installPressed), forControlEvents: .touchUpInside)
}
deinit {
self.preloadDisposable.dispose()
self.preloadedStickerPackThumbnailDisposable.dispose()
}
public override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
private var absoluteLocation: (CGRect, CGSize)?
public override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteLocation = (rect, containerSize)
for node in self.itemNodes {
let nodeRect = CGRect(origin: CGPoint(x: rect.minX + node.frame.minX, y: rect.minY + node.frame.minY), size: node.frame.size)
node.updateAbsoluteRect(nodeRect, within: containerSize)
}
}
public func setup(item: StickerPaneSearchGlobalItem) {
if item.topItems.count < Int(item.info.count) && item.topItems.count < 5 && self.item?.info.id != item.info.id {
self.preloadDisposable.set(preloadedFeaturedStickerSet(network: item.context.account.network, postbox: item.context.account.postbox, id: item.info.id).start())
}
self.item = item
self.setNeedsLayout()
self.updatePreviewing(animated: false)
}
public func updateCanPlayMedia() {
guard let item = self.item else {
return
}
self.canPlayMedia = item.itemContext.canPlayMedia && item.context.sharedContext.energyUsageSettings.loopStickers
}
public func highlight() {
guard self.highlightNode == nil else {
return
}
let highlightNode = ASDisplayNode()
highlightNode.frame = self.bounds
if let theme = self.item?.theme {
highlightNode.backgroundColor = theme.list.itemCheckColors.fillColor.withAlphaComponent(0.08)
}
self.highlightNode = highlightNode
self.insertSubnode(highlightNode, at: 0)
Queue.mainQueue().after(1.5) {
self.highlightNode = nil
highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak highlightNode] _ in
highlightNode?.removeFromSupernode()
})
}
}
public override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
guard let item = self.item else {
return
}
let params = ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: size.height)
let topSeparatorOffset: CGFloat
var topOffset: CGFloat = 0.0
if item.regularInsets {
topOffset = 12.0
topSeparatorOffset = -UIScreenPixel
} else {
topSeparatorOffset = 16.0
topOffset += 12.0
if item.topSeparator {
topOffset += 12.0
}
}
self.topSeparatorNode.isHidden = !item.topSeparator
self.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 16.0, y: topSeparatorOffset), size: CGSize(width: params.width - 16.0 * 2.0, height: UIScreenPixel))
if item.listAppearance {
self.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
} else {
self.topSeparatorNode.backgroundColor = item.theme.chat.inputMediaPanel.stickersSectionTextColor.withAlphaComponent(0.3)
}
let makeInstallLayout = TextNode.asyncLayout(self.installTextNode)
let makeUninstallLayout = TextNode.asyncLayout(self.uninstallTextNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
let currentItem = self.appliedItem
self.appliedItem = item
var updateButtonBackgroundImage: UIImage?
var updateUninstallButtonBackgroundImage: UIImage?
if currentItem?.theme !== item.theme {
updateUninstallButtonBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddedPackButtonImage(item.theme)
updateButtonBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddPackButtonImage(item.theme)
}
let unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(item.theme)
let leftInset: CGFloat = 14.0
let rightInset: CGFloat = 16.0
let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_Install, font: buttonFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (uninstallLayout, uninstallApply) = makeUninstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_Installed, font: buttonFont, textColor: item.theme.list.itemCheckColors.fillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.info.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0 - max(installLayout.size.width, uninstallLayout.size.width), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.StickerPack_StickerCount(item.info.count), font: statusFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let strongSelf = self
let _ = installApply()
let _ = uninstallApply()
let _ = titleApply()
let _ = descriptionApply()
if let updateButtonBackgroundImage = updateButtonBackgroundImage {
strongSelf.installBackgroundNode.image = updateButtonBackgroundImage
}
if let updateUninstallButtonBackgroundImage = updateUninstallButtonBackgroundImage {
strongSelf.uninstallBackgroundNode.image = updateUninstallButtonBackgroundImage
}
let installWidth: CGFloat = installLayout.size.width + 32.0
let buttonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - installWidth, y: 4.0 + topOffset), size: CGSize(width: installWidth, height: 28.0))
strongSelf.installBackgroundNode.frame = buttonFrame
strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size)
strongSelf.installButtonNode.frame = buttonFrame
let uninstallWidth: CGFloat = uninstallLayout.size.width + 32.0
let uninstallButtonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - uninstallWidth, y: 4.0 + topOffset), size: CGSize(width: uninstallWidth, height: 28.0))
strongSelf.uninstallBackgroundNode.frame = uninstallButtonFrame
strongSelf.uninstallTextNode.frame = CGRect(origin: CGPoint(x: uninstallButtonFrame.minX + floor((uninstallButtonFrame.width - uninstallLayout.size.width) / 2.0), y: uninstallButtonFrame.minY + floor((uninstallButtonFrame.height - uninstallLayout.size.height) / 2.0) + 1.0), size: uninstallLayout.size)
strongSelf.uninstallButtonNode.frame = uninstallButtonFrame
strongSelf.installButtonNode.isHidden = item.installed
strongSelf.installBackgroundNode.isHidden = item.installed
strongSelf.installTextNode.isHidden = item.installed
strongSelf.uninstallButtonNode.isHidden = !item.installed
strongSelf.uninstallBackgroundNode.isHidden = !item.installed
strongSelf.uninstallTextNode.isHidden = !item.installed
let titleFrame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: 2.0 + topOffset), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: 23.0 + topOffset), size: descriptionLayout.size)
if item.unread {
strongSelf.unreadNode.isHidden = false
} else {
strongSelf.unreadNode.isHidden = true
}
if let image = unreadImage {
strongSelf.unreadNode.image = image
strongSelf.unreadNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: titleFrame.minY + 7.0), size: image.size)
}
let sideInset: CGFloat = 2.0
let availableWidth = params.width - params.leftInset - params.rightInset - sideInset * 2.0
var itemSide: CGFloat = floor(availableWidth / 5.0)
itemSide = min(itemSide, 75.0)
let itemSize = CGSize(width: itemSide, height: itemSide)
var offset = sideInset
let itemSpacing = (max(0, availableWidth - 5.0 * itemSide - sideInset * 2.0)) / 4.0
var topItems = item.topItems
if topItems.count > 5 {
topItems.removeSubrange(5 ..< topItems.count)
}
for i in 0 ..< topItems.count {
let file = topItems[i].file
let node: TrendingTopItemNode
if i < strongSelf.itemNodes.count {
node = strongSelf.itemNodes[i]
} else {
node = TrendingTopItemNode()
node.visibility = strongSelf.canPlay
strongSelf.itemNodes.append(node)
strongSelf.addSubnode(node)
}
if file.fileId != node.file?.fileId {
node.setup(account: item.context.account, item: topItems[i], itemSize: itemSize, synchronousLoads: synchronousLoads)
}
if item.theme !== node.theme {
node.update(theme: item.theme, listAppearance: item.listAppearance)
}
if let dimensions = file.dimensions {
let imageSize = dimensions.cgSize.aspectFitted(itemSize)
node.frame = CGRect(origin: CGPoint(x: offset, y: 48.0 + topOffset), size: imageSize)
offset += itemSize.width + itemSpacing
}
if let (rect, size) = strongSelf.absoluteLocation {
strongSelf.updateAbsoluteRect(rect, within: size)
}
}
if topItems.count < strongSelf.itemNodes.count {
for i in (topItems.count ..< strongSelf.itemNodes.count).reversed() {
strongSelf.itemNodes[i].removeFromSupernode()
strongSelf.itemNodes.remove(at: i)
}
}
self.canPlayMedia = item.itemContext.canPlayMedia && item.context.sharedContext.energyUsageSettings.loopStickers
}
@objc func installPressed() {
if let item = self.item {
item.install()
}
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let item = self.item {
item.open()
}
}
}
public func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
guard let item = self.item else {
return nil
}
var index = 0
for itemNode in self.itemNodes {
if itemNode.frame.contains(point), index < item.topItems.count {
return (itemNode, item.topItems[index])
}
index += 1
}
return nil
}
public func updatePreviewing(animated: Bool) {
guard let item = self.item else {
return
}
var index = 0
for itemNode in self.itemNodes {
if index < item.topItems.count {
let isPreviewing = item.getItemIsPreviewed(item.topItems[index])
itemNode.updatePreviewing(animated: animated, isPreviewing: isPreviewing)
}
index += 1
}
}
}