mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-12 18:14:10 +01:00
435 lines
18 KiB
Swift
435 lines
18 KiB
Swift
import Foundation
|
|
import LottieSettings
|
|
import UIKit
|
|
import Display
|
|
import AccountContext
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
import GZip
|
|
import TelegramPresentationData
|
|
import ComponentFlow
|
|
import BundleIconComponent
|
|
import MultilineTextComponent
|
|
import LottieComponent
|
|
import WalletCollectibleImageComponent
|
|
import GiftItemComponent
|
|
|
|
public func walletCollectibleFragmentUrl(_ value: String?) -> String? {
|
|
guard let value,
|
|
let components = URLComponents(string: value),
|
|
components.scheme?.lowercased() == "https",
|
|
components.host?.lowercased() == "fragment.com",
|
|
let url = components.url else {
|
|
return nil
|
|
}
|
|
return url.absoluteString
|
|
}
|
|
|
|
private let walletCollectibleLottieMaximumSize = 5 * 1024 * 1024
|
|
|
|
public func walletCollectibleLottieData(_ data: Data) -> Data? {
|
|
guard data.count <= walletCollectibleLottieMaximumSize else { return nil }
|
|
let decoded: Data
|
|
if data.starts(with: [0x1f, 0x8b]) {
|
|
guard let unpacked = TGGUnzipData(data, UInt32(walletCollectibleLottieMaximumSize)) else { return nil }
|
|
decoded = unpacked
|
|
} else {
|
|
decoded = data
|
|
}
|
|
guard decoded.count <= walletCollectibleLottieMaximumSize,
|
|
(try? JSONSerialization.jsonObject(with: decoded)) is [String: Any] else { return nil }
|
|
return decoded
|
|
}
|
|
|
|
public final class WalletRemoteLottieContent: LottieComponent.Content {
|
|
let context: AccountContext
|
|
let file: WalletNftFile
|
|
|
|
override public var frameRange: Range<Double> { return 0.0 ..< 1.0 }
|
|
// RLottie writes sRGB values directly; tagging them as Display P3 changes
|
|
// their appearance without converting the pixels and oversaturates gifts.
|
|
override public var renderingColorSpace: CGColorSpace? { CGColorSpace(name: CGColorSpace.sRGB) }
|
|
|
|
public init(context: AccountContext, file: WalletNftFile) {
|
|
self.context = context
|
|
self.file = file
|
|
super.init()
|
|
}
|
|
|
|
override public func isEqual(to other: LottieComponent.Content) -> Bool {
|
|
guard let other = other as? WalletRemoteLottieContent else { return false }
|
|
return self.context === other.context && self.file == other.file
|
|
}
|
|
|
|
override public func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable {
|
|
let resources = self.context.engine.resources
|
|
let resource = self.file.resource
|
|
return Signal<Data, NoError> { subscriber in
|
|
let dataDisposable = (resources.data(resource: EngineMediaResource(resource))
|
|
|> deliverOn(Queue.concurrentDefaultQueue())).start(next: { resourceData in
|
|
guard resourceData.availableSize <= Int64(walletCollectibleLottieMaximumSize) else {
|
|
subscriber.putCompletion()
|
|
return
|
|
}
|
|
guard resourceData.isComplete else { return }
|
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: .mappedIfSafe),
|
|
let decoded = walletCollectibleLottieData(data) {
|
|
subscriber.putNext(decoded)
|
|
}
|
|
subscriber.putCompletion()
|
|
})
|
|
let fetchDisposable = resources.fetchedResource(resource: EngineMediaResource(resource)).start(error: { _ in
|
|
subscriber.putCompletion()
|
|
})
|
|
return ActionDisposable {
|
|
dataDisposable.dispose()
|
|
fetchDisposable.dispose()
|
|
}
|
|
}.start(next: { data in
|
|
f(.animation(data: data, cacheKey: resource.id.stringRepresentation))
|
|
})
|
|
}
|
|
}
|
|
|
|
public final class WalletCollectibleHeaderComponent: Component {
|
|
public typealias EnvironmentType = Empty
|
|
|
|
public struct Item: Equatable {
|
|
public let name: String
|
|
public let image: WalletNftFile?
|
|
public let lottie: WalletNftFile?
|
|
public let collectionName: String?
|
|
public let collectionUrl: String?
|
|
public let uniqueGift: StarGift.UniqueGift?
|
|
|
|
public init(
|
|
name: String,
|
|
image: WalletNftFile?,
|
|
lottie: WalletNftFile?,
|
|
collectionName: String?,
|
|
collectionUrl: String?,
|
|
uniqueGift: StarGift.UniqueGift? = nil
|
|
) {
|
|
self.name = name
|
|
self.image = image
|
|
self.lottie = lottie
|
|
self.collectionName = collectionName
|
|
self.collectionUrl = collectionUrl
|
|
self.uniqueGift = uniqueGift
|
|
}
|
|
}
|
|
|
|
public let context: AccountContext
|
|
public let theme: PresentationTheme
|
|
public let item: Item
|
|
public let displaysCollection: Bool
|
|
public let openCollection: (String) -> Void
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
theme: PresentationTheme,
|
|
item: Item,
|
|
displaysCollection: Bool = true,
|
|
openCollection: @escaping (String) -> Void
|
|
) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.item = item
|
|
self.displaysCollection = displaysCollection
|
|
self.openCollection = openCollection
|
|
}
|
|
|
|
public static func ==(lhs: WalletCollectibleHeaderComponent, rhs: WalletCollectibleHeaderComponent) -> Bool {
|
|
return lhs.context === rhs.context
|
|
&& lhs.theme === rhs.theme
|
|
&& lhs.item == rhs.item
|
|
&& lhs.displaysCollection == rhs.displaysCollection
|
|
}
|
|
|
|
public final class View: UIView {
|
|
private let image = ComponentView<Empty>()
|
|
private let gift = ComponentView<Empty>()
|
|
private var lottie = ComponentView<Empty>()
|
|
private var lottieFile: WalletNftFile?
|
|
private weak var lottieContext: AccountContext?
|
|
private let title = ComponentView<Empty>()
|
|
private let collection = ComponentView<Empty>()
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc private func giftTapped() {
|
|
(self.gift.view as? GiftItemComponent.View)?.playModelOnce()
|
|
}
|
|
|
|
@objc private func lottieTapped() {
|
|
(self.lottie.view as? LottieComponent.View)?.playOnce()
|
|
}
|
|
|
|
public var giftView: UIView? {
|
|
guard let view = self.gift.view, view.superview === self else { return nil }
|
|
return view
|
|
}
|
|
|
|
private var silhouetteView: UIImageView?
|
|
|
|
public func showModelSilhouette(image: UIImage, frame: CGRect, color: UIColor) {
|
|
guard let giftView = self.gift.view as? GiftItemComponent.View else { return }
|
|
giftView.setModelHidden(true)
|
|
let silhouetteView = self.silhouetteView ?? UIImageView()
|
|
self.silhouetteView = silhouetteView
|
|
silhouetteView.isUserInteractionEnabled = false
|
|
silhouetteView.image = image.withRenderingMode(.alwaysTemplate)
|
|
silhouetteView.tintColor = color
|
|
silhouetteView.frame = frame
|
|
if silhouetteView.superview !== giftView {
|
|
giftView.addSubview(silhouetteView)
|
|
}
|
|
}
|
|
|
|
public func setAnimationVisible(_ value: Bool) {
|
|
(self.lottie.view as? LottieComponent.View)?.externalShouldPlay = value
|
|
}
|
|
|
|
fileprivate func update(
|
|
component: WalletCollectibleHeaderComponent,
|
|
availableSize: CGSize,
|
|
transition: ComponentTransition
|
|
) -> CGSize {
|
|
let mediaSide = min(164.0, max(0.0, availableSize.width - 64.0))
|
|
let mediaSize = CGSize(width: mediaSide, height: mediaSide)
|
|
let mediaFrame = CGRect(
|
|
x: floorToScreenPixels((availableSize.width - mediaSide) / 2.0),
|
|
y: 0.0,
|
|
width: mediaSide,
|
|
height: mediaSide
|
|
)
|
|
let mediaCornerRadius: CGFloat = 32.0
|
|
if let uniqueGift = component.item.uniqueGift {
|
|
let _ = self.gift.update(
|
|
transition: transition,
|
|
component: AnyComponent(GiftItemComponent(
|
|
context: component.context,
|
|
theme: component.theme,
|
|
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings,
|
|
subject: .uniqueGift(gift: uniqueGift, price: nil),
|
|
mode: .grid,
|
|
cornerRadius: mediaCornerRadius,
|
|
allowAnimations: true
|
|
)),
|
|
environment: {},
|
|
containerSize: mediaSize
|
|
)
|
|
if let giftView = self.gift.view {
|
|
if giftView.superview == nil {
|
|
giftView.isUserInteractionEnabled = true
|
|
giftView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.giftTapped)))
|
|
self.addSubview(giftView)
|
|
}
|
|
transition.setFrame(view: giftView, frame: mediaFrame)
|
|
}
|
|
self.lottie.view?.isHidden = true
|
|
} else {
|
|
self.gift.view?.removeFromSuperview()
|
|
self.lottie.view?.isHidden = false
|
|
}
|
|
let _ = self.image.update(
|
|
transition: transition,
|
|
component: AnyComponent(WalletCollectibleImageComponent(
|
|
context: component.context,
|
|
file: component.item.image,
|
|
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
|
cornerRadius: mediaCornerRadius
|
|
)),
|
|
environment: {},
|
|
containerSize: mediaSize
|
|
)
|
|
if let imageView = self.image.view {
|
|
if imageView.superview == nil {
|
|
imageView.isUserInteractionEnabled = false
|
|
self.addSubview(imageView)
|
|
}
|
|
imageView.isHidden = component.item.uniqueGift != nil
|
|
transition.setFrame(view: imageView, frame: mediaFrame)
|
|
}
|
|
|
|
if self.lottieFile != component.item.lottie || self.lottieContext !== component.context {
|
|
self.setAnimationVisible(false)
|
|
self.lottie.view?.removeFromSuperview()
|
|
self.lottie = ComponentView<Empty>()
|
|
self.lottieFile = component.item.lottie
|
|
self.lottieContext = component.context
|
|
}
|
|
if component.item.uniqueGift == nil, let lottie = component.item.lottie {
|
|
let lottieContent = WalletRemoteLottieContent(context: component.context, file: lottie)
|
|
let lottieSize = self.lottie.update(
|
|
transition: transition,
|
|
component: AnyComponent(LottieComponent(
|
|
content: lottieContent,
|
|
startingPosition: .begin,
|
|
size: mediaSize,
|
|
loop: false,
|
|
lottieSettings: component.context.lottieRenderingSettings
|
|
)),
|
|
environment: {},
|
|
containerSize: mediaSize
|
|
)
|
|
if let lottieView = self.lottie.view as? LottieComponent.View {
|
|
if lottieView.superview == nil {
|
|
lottieView.isUserInteractionEnabled = true
|
|
lottieView.addGestureRecognizer(UITapGestureRecognizer(
|
|
target: self,
|
|
action: #selector(self.lottieTapped)
|
|
))
|
|
self.addSubview(lottieView)
|
|
lottieView.playOnce()
|
|
}
|
|
lottieView.clipsToBounds = true
|
|
lottieView.layer.cornerRadius = mediaCornerRadius
|
|
transition.setFrame(view: lottieView, frame: CGRect(origin: mediaFrame.origin, size: lottieSize))
|
|
transition.setAlpha(view: lottieView, alpha: 1.0)
|
|
}
|
|
self.setAnimationVisible(true)
|
|
}
|
|
|
|
var contentHeight = mediaSize.height + 18.0
|
|
let textWidth = max(0.0, availableSize.width - 48.0)
|
|
let titleText = NSMutableAttributedString(attributedString: NSAttributedString(
|
|
string: component.item.name,
|
|
font: Font.semibold(20.0),
|
|
textColor: component.theme.actionSheet.primaryTextColor
|
|
))
|
|
if let numberRange = component.item.name.range(of: "#[0-9]+$", options: .regularExpression) {
|
|
titleText.addAttribute(
|
|
.foregroundColor,
|
|
value: component.theme.actionSheet.secondaryTextColor,
|
|
range: NSRange(numberRange, in: component.item.name)
|
|
)
|
|
}
|
|
let titleSize = self.title.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(titleText),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 2
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: textWidth, height: 100.0)
|
|
)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
titleView.isUserInteractionEnabled = false
|
|
self.addSubview(titleView)
|
|
}
|
|
transition.setFrame(view: titleView, frame: CGRect(
|
|
x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0),
|
|
y: contentHeight,
|
|
width: titleSize.width,
|
|
height: titleSize.height
|
|
))
|
|
}
|
|
contentHeight += titleSize.height
|
|
|
|
if component.displaysCollection,
|
|
let collectionName = component.item.collectionName,
|
|
!collectionName.isEmpty {
|
|
let collectionUrl = walletCollectibleFragmentUrl(component.item.collectionUrl)
|
|
var collectionItems: [AnyComponentWithIdentity<Empty>] = [
|
|
AnyComponentWithIdentity(id: "title", component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: collectionName,
|
|
font: Font.regular(15.0),
|
|
textColor: component.theme.actionSheet.secondaryTextColor
|
|
)),
|
|
maximumNumberOfLines: 1
|
|
)))
|
|
]
|
|
if collectionUrl != nil {
|
|
collectionItems.append(AnyComponentWithIdentity(
|
|
id: "disclosure",
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: "Wallet/Chevron",
|
|
tintColor: component.theme.actionSheet.secondaryTextColor
|
|
))
|
|
))
|
|
}
|
|
let collectionContent: AnyComponent<Empty> = AnyComponent(HStack(collectionItems, spacing: 4.0))
|
|
let collectionComponent: AnyComponent<Empty>
|
|
if let collectionUrl {
|
|
collectionComponent = AnyComponent(Button(
|
|
content: collectionContent,
|
|
action: {
|
|
component.openCollection(collectionUrl)
|
|
}
|
|
))
|
|
} else {
|
|
collectionComponent = collectionContent
|
|
}
|
|
let collectionSize = self.collection.update(
|
|
transition: transition,
|
|
component: collectionComponent,
|
|
environment: {},
|
|
containerSize: CGSize(width: textWidth, height: 40.0)
|
|
)
|
|
contentHeight += 6.0
|
|
if let collectionView = self.collection.view {
|
|
if collectionView.superview == nil {
|
|
self.addSubview(collectionView)
|
|
}
|
|
transition.setFrame(view: collectionView, frame: CGRect(
|
|
x: floorToScreenPixels((availableSize.width - collectionSize.width) / 2.0),
|
|
y: contentHeight,
|
|
width: collectionSize.width,
|
|
height: collectionSize.height
|
|
))
|
|
transition.setAlpha(view: collectionView, alpha: 1.0)
|
|
}
|
|
contentHeight += collectionSize.height
|
|
} else if let collectionView = self.collection.view {
|
|
transition.setAlpha(view: collectionView, alpha: 0.0)
|
|
}
|
|
|
|
return CGSize(width: availableSize.width, height: contentHeight)
|
|
}
|
|
|
|
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
if let result,
|
|
let lottieView = self.lottie.view,
|
|
result === lottieView || result.isDescendant(of: lottieView) {
|
|
return result
|
|
}
|
|
if let result,
|
|
let giftView = self.gift.view, giftView.superview === self,
|
|
result === giftView || result.isDescendant(of: giftView) {
|
|
return giftView
|
|
}
|
|
if let result,
|
|
let collectionView = self.collection.view,
|
|
result === collectionView || result.isDescendant(of: collectionView) {
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(
|
|
view: View,
|
|
availableSize: CGSize,
|
|
state: EmptyComponentState,
|
|
environment: Environment<Empty>,
|
|
transition: ComponentTransition
|
|
) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
|
}
|
|
}
|