Files
Swiftgram/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift
Ilya Laktyushin a1aa4d9094 Various fixes
2026-01-28 19:05:47 +04:00

794 lines
35 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import GiftItemComponent
import GlassBackgroundComponent
import GlassBarButtonComponent
import BundleIconComponent
import LottieComponent
private let cubeSide: CGFloat = 110.0
struct GiftItem: Equatable {
let gift: StarGift.UniqueGift
let reference: StarGiftReference
}
final class CraftTableComponent: Component {
enum Result {
case gift(ProfileGiftsContext.State.StarGift)
case fail
}
let context: AccountContext
let gifts: [Int32: GiftItem]
let buttonColor: UIColor
let isCrafting: Bool
let result: Result?
let select: (Int32) -> Void
let remove: (Int32) -> Void
let willFinish: (Bool) -> Void
let finished: (UIView?) -> Void
public init(
context: AccountContext,
gifts: [Int32: GiftItem],
buttonColor: UIColor,
isCrafting: Bool,
result: Result?,
select: @escaping (Int32) -> Void,
remove: @escaping (Int32) -> Void,
willFinish: @escaping (Bool) -> Void,
finished: @escaping (UIView?) -> Void
) {
self.context = context
self.gifts = gifts
self.buttonColor = buttonColor
self.isCrafting = isCrafting
self.result = result
self.select = select
self.remove = remove
self.willFinish = willFinish
self.finished = finished
}
public static func ==(lhs: CraftTableComponent, rhs: CraftTableComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gifts != rhs.gifts {
return false
}
if lhs.buttonColor != rhs.buttonColor {
return false
}
if lhs.isCrafting != rhs.isCrafting {
return false
}
return true
}
public final class View: UIView {
private var selectedGifts: [AnyHashable: ComponentView<Empty>] = [:]
private var faces: [AnyHashable: ComponentView<Empty>] = [:]
private let successFace = ComponentView<Empty>()
private let anvilPlayOnce = ActionSlot<Void>()
private let animationView = CubeAnimationView()
private let failOverlay = ComponentView<Empty>()
private let craftFailOverlayPlayOnce = ActionSlot<Void>()
private let craftFailPlayOnce = ActionSlot<Void>()
private var didSetupFinishAnimation = false
private var flipFaces = false
private var isSuccess = false
private var isFailed = false
private var failDidStartCrossAnimation = false
private var failDidBringToFront = false
private var failWillFinish = false
private var failDidFinish = false
private var component: CraftTableComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.animationView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupFailureAnimation() {
guard !self.didSetupFinishAnimation else {
return
}
self.didSetupFinishAnimation = true
self.animationView.onFinishApproach = { [weak self] isUpsideDown in
guard let self else {
return
}
self.isFailed = true
self.animationView.setSticker(nil, face: 0, mirror: false)
var availableStickers: [ComponentView<Empty>] = []
for gift in self.selectedGifts.values {
availableStickers.append(gift)
}
for i in 0 ..< min(2, availableStickers.count) {
if let sticker = availableStickers[i].view {
let face: Int
if isUpsideDown {
face = i + 1
} else {
face = 3 - i
}
self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown)
}
}
self.flipFaces = isUpsideDown
Queue.mainQueue().after(0.3, {
self.failWillFinish = true
self.component?.willFinish(false)
})
Queue.mainQueue().after(0.5, {
self.failDidFinish = true
self.component?.finished(nil)
})
self.state?.updated(transition: .easeInOut(duration: 0.4))
}
}
func setupSuccessAnimation(_ gift: StarGift.UniqueGift) {
guard !self.didSetupFinishAnimation, let component = self.component else {
return
}
self.didSetupFinishAnimation = true
self.animationView.isSuccess = true
self.animationView.onFinishApproach = { [weak self] isUpsideDown in
guard let self else {
return
}
self.isSuccess = true
var availableStickers: [ComponentView<Empty>] = []
for gift in self.selectedGifts.values {
availableStickers.append(gift)
}
for i in 0 ..< min(2, availableStickers.count) {
if let sticker = availableStickers[i].view {
let face: Int
if isUpsideDown {
face = i + 1
} else {
face = 3 - i
}
self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown)
}
}
self.flipFaces = isUpsideDown
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let _ = self.successFace.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: component.context,
style: .glass,
theme: presentationData.theme,
strings: presentationData.strings,
peer: nil,
subject: .uniqueGift(gift: gift, price: nil),
ribbon: nil,
resellPrice: nil,
isHidden: false,
isSelected: false,
isPinned: false,
isEditing: false,
mode: .grid,
cornerRadius: 28.0,
action: nil,
contextAction: nil
)
),
environment: {},
containerSize: CGSize(width: cubeSide, height: cubeSide)
)
if let successView = self.successFace.view as? GiftItemComponent.View {
let backgroundLayer = successView.backgroundLayer
if let patternView = successView.pattern {
backgroundLayer.opacity = 0.0
patternView.alpha = 0.0
Queue.mainQueue().after(1.0, {
let transition = ComponentTransition.easeInOut(duration: 0.3)
transition.animateBlur(layer: backgroundLayer, fromRadius: 10.0, toRadius: 0.0)
transition.setAlpha(layer: backgroundLayer, alpha: 1.0)
transition.setAlpha(view: patternView, alpha: 1.0)
transition.animateBlur(layer: patternView.layer, fromRadius: 10.0, toRadius: 0.0)
Queue.mainQueue().after(1.0, {
self.component?.finished(successView)
})
})
}
self.animationView.setSticker(successView, face: 0, mirror: isUpsideDown)
}
self.state?.updated()
}
}
func update(component: CraftTableComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
self.animationView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
let permilleValue = component.gifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) })
for index in 0 ..< 6 {
let face: ComponentView<Empty>
if let current = self.faces[index] {
face = current
} else {
face = ComponentView<Empty>()
self.faces[index] = face
}
let faceComponent: AnyComponent<Empty>
var faceItems: [AnyComponentWithIdentity<Empty>] = []
if index == 0 {
faceItems.append(
AnyComponentWithIdentity(id: "background", component: AnyComponent(
FilledRoundedRectangleComponent(color: component.buttonColor, cornerRadius: .value(28.0), smoothCorners: true)
))
)
if !component.isCrafting || self.isFailed {
faceItems.append(
AnyComponentWithIdentity(id: "glass", component: AnyComponent(
GlassBackgroundComponent(size: CGSize(width: cubeSide, height: cubeSide), cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)))
))
)
}
if self.isFailed {
faceItems.append(
AnyComponentWithIdentity(id: "faildial", component: AnyComponent(
DialIndicatorComponent(
content: AnyComponentWithIdentity(id: "gift", component: AnyComponent(BundleIconComponent(name: "Premium/GiftCrash", tintColor: .white))),
backgroundColor: .white.withAlphaComponent(0.1),
foregroundColor: .white,
diameter: 84.0,
contentSize: CGSize(width: 44.0, height: 44.0),
lineWidth: 5.0,
fontSize: 18.0,
progress: 0.0,
value: component.gifts.count,
suffix: "",
isVisible: true,
isFlipped: self.flipFaces
)
))
)
} else if !self.isSuccess {
faceItems.append(
AnyComponentWithIdentity(id: "dial", component: AnyComponent(
DialIndicatorComponent(
content: AnyComponentWithIdentity(id: "empty", component: AnyComponent(Rectangle(color: .clear))),
backgroundColor: .white.withAlphaComponent(0.1),
foregroundColor: .white,
diameter: 84.0,
lineWidth: 5.0,
fontSize: 18.0,
progress: CGFloat(permilleValue / 10 / 100),
value: permilleValue / 10,
suffix: "%",
isVisible: !component.isCrafting
)
))
)
faceItems.append(
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
LottieComponent(
content: LottieComponent.AppBundleContent(name: "Anvil"),
size: CGSize(width: 52.0, height: 52.0),
playOnce: self.anvilPlayOnce
)
))
)
}
} else {
faceItems.append(
AnyComponentWithIdentity(id: "background", component: AnyComponent(
FilledRoundedRectangleComponent(color: component.buttonColor, cornerRadius: .value(28.0), smoothCorners: true)
))
)
faceItems.append(
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
BundleIconComponent(name: "Components/CubeSide", tintColor: nil, flipVertically: self.flipFaces)
))
)
}
faceComponent = AnyComponent(
ZStack(faceItems)
)
let _ = face.update(
transition: transition,
component: faceComponent,
environment: {},
containerSize: CGSize(width: cubeSide, height: cubeSide)
)
}
if previousComponent == nil {
var faceViews: [UIView] = []
for index in 0 ..< 6 {
if let faceView = self.faces[index]?.view {
faceView.bounds = CGRect(origin: .zero, size: CGSize(width: cubeSide, height: cubeSide))
faceView.clipsToBounds = true
faceView.layer.rasterizationScale = UIScreenScale
faceView.layer.cornerRadius = 28.0
faceViews.append(faceView)
}
}
self.animationView.setFaces(faceViews)
}
var stickerViews: [UIView] = []
for index in 0 ..< 4 {
let itemId = AnyHashable(index)
var itemTransition = transition
let visibleItem: ComponentView<Empty>
if let current = self.selectedGifts[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
self.selectedGifts[itemId] = visibleItem
itemTransition = .immediate
}
let gift = component.gifts[Int32(index)]
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
GiftSlotComponent(
context: component.context,
gift: gift,
buttonColor: component.buttonColor,
isCrafting: component.isCrafting,
action: {
component.select(Int32(index))
},
removeAction: index > 0 ? {
component.remove(Int32(index))
} : nil
)
),
environment: {},
containerSize: CGSize(width: cubeSide, height: cubeSide)
)
if let itemView = visibleItem.view {
stickerViews.append(itemView)
}
}
if previousComponent == nil {
self.animationView.setStickers(stickerViews)
}
if let previousComponent, previousComponent.isCrafting != component.isCrafting {
var indices: [Int] = []
for index in component.gifts.keys.sorted() {
indices.append(Int(index))
}
self.anvilPlayOnce.invoke(Void())
Queue.mainQueue().after(0.6, {
self.animationView.startStickerSequence(indices: indices)
switch component.result {
case let .gift(gift):
if case let .unique(uniqueGift) = gift.gift {
self.setupSuccessAnimation(uniqueGift)
}
case .fail:
self.setupFailureAnimation()
default:
break
}
})
}
// if self.isFailed {
// let failOverlaySize = self.failOverlay.update(
// transition: .immediate,
// component: AnyComponent(
// LottieComponent(
// content: LottieComponent.AppBundleContent(name: "CraftFailOverlay"),
// size: CGSize(width: availableSize.width, height: availableSize.width),
// playOnce: self.craftFailOverlayPlayOnce
// )
// ),
// environment: {},
// containerSize: CGSize(width: availableSize.width, height: availableSize.width)
// )
// let failOverlayFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - failOverlaySize.width) / 2.0), y: floor((availableSize.height - failOverlaySize.height) / 2.0)), size: failOverlaySize)
// if let failOverlayView = self.failOverlay.view {
// if failOverlayView.superview == nil {
// failOverlayView.isHidden = true
// self.insertSubview(failOverlayView, belowSubview: self.animationView)
// }
// failOverlayView.frame = failOverlayFrame
// }
// }
return availableSize
}
}
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, state: state, environment: environment, transition: transition)
}
}
final class GiftSlotComponent: Component {
let context: AccountContext
let gift: GiftItem?
let buttonColor: UIColor
let isCrafting: Bool
let action: () -> Void
let removeAction: (() -> Void)?
public init(
context: AccountContext,
gift: GiftItem?,
buttonColor: UIColor,
isCrafting: Bool,
action: @escaping () -> Void,
removeAction: (() -> Void)?
) {
self.context = context
self.gift = gift
self.buttonColor = buttonColor
self.isCrafting = isCrafting
self.action = action
self.removeAction = removeAction
}
public static func ==(lhs: GiftSlotComponent, rhs: GiftSlotComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.buttonColor != rhs.buttonColor {
return false
}
if lhs.isCrafting != rhs.isCrafting {
return false
}
return true
}
public final class View: UIView {
private let backgroundView = GlassBackgroundView()
private let addIcon = UIImageView()
private var icon: ComponentView<Empty>?
private let button = HighlightTrackingButton()
private var badge: ComponentView<Empty>?
private var removeIcon: ComponentView<Empty>?
private let removeButton = HighlightTrackingButton()
private var component: GiftSlotComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.addIcon.image = generateAddIcon(backgroundColor: .white)
self.addSubview(self.backgroundView)
self.backgroundView.contentView.addSubview(self.addIcon)
self.backgroundView.contentView.addSubview(self.button)
self.addSubview(self.removeButton)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.removeButton.addTarget(self, action: #selector(self.removeButtonPressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
guard let _ = self.component?.removeAction else {
return
}
self.component?.action()
}
@objc private func removeButtonPressed() {
self.component?.removeAction?()
}
func update(component: GiftSlotComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
let backgroundFrame = CGRect(origin: .zero, size: availableSize).insetBy(dx: 1.0, dy: 1.0)
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), isInteractive: true, transition: .immediate)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
if component.gift == nil && component.isCrafting && previousComponent?.isCrafting == false {
transition.setBlur(layer: self.backgroundView.layer, radius: 10.0)
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false)
transition.setBlur(layer: self.addIcon.layer, radius: 10.0)
}
transition.setAlpha(view: self.addIcon, alpha: component.isCrafting ? 0.0 : 1.0)
if let icon = self.addIcon.image {
transition.setFrame(view: self.addIcon, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - icon.size.width) / 2.0), y: floor((backgroundFrame.height - icon.size.height) / 2.0)), size: icon.size))
}
if previousComponent?.gift?.gift.id != component.gift?.gift.id {
if let iconView = self.icon?.view {
if transition.animation.isImmediate {
iconView.removeFromSuperview()
} else {
transition.setScale(view: iconView, scale: 0.01)
transition.setAlpha(view: iconView, alpha: 0.0, completion: { _ in
iconView.removeFromSuperview()
})
}
}
self.icon = nil
}
if (previousComponent?.gift?.gift.id == nil) != (component.gift?.gift.id == nil) || ((previousComponent?.isCrafting ?? false) != component.isCrafting && component.isCrafting) {
if let badgeView = self.badge?.view {
if transition.animation.isImmediate {
badgeView.removeFromSuperview()
} else {
transition.setBlur(layer: badgeView.layer, radius: 10.0)
transition.setAlpha(view: badgeView, alpha: 0.0, completion: { _ in
badgeView.removeFromSuperview()
})
}
}
self.badge = nil
if let removeButtonView = self.removeIcon?.view {
if transition.animation.isImmediate {
removeButtonView.removeFromSuperview()
} else {
transition.setBlur(layer: removeButtonView.layer, radius: 10.0)
transition.setAlpha(view: removeButtonView, alpha: 0.0, completion: { _ in
removeButtonView.removeFromSuperview()
})
}
}
self.removeIcon = nil
}
if let gift = component.gift {
let icon: ComponentView<Empty>
var iconTransition = transition
if let current = self.icon {
icon = current
} else {
iconTransition = .immediate
icon = ComponentView()
self.icon = icon
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let iconSize = icon.update(
transition: iconTransition,
component: AnyComponent(
GiftItemComponent(
context: component.context,
style: .glass,
theme: presentationData.theme,
strings: presentationData.strings,
peer: nil,
subject: .uniqueGift(gift: gift.gift, price: nil),
ribbon: nil,
resellPrice: nil,
isHidden: false,
isSelected: false,
isPinned: false,
isEditing: false,
mode: .grid,
cornerRadius: 28.0,
action: nil,
contextAction: nil
)
),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: iconSize)
if let iconView = icon.view {
if iconView.superview == nil {
iconView.isUserInteractionEnabled = false
if let badgeView = self.badge?.view {
self.backgroundView.contentView.insertSubview(iconView, belowSubview: badgeView)
} else {
self.backgroundView.contentView.addSubview(iconView)
}
if !transition.animation.isImmediate {
transition.animateAlpha(view: iconView, from: 0.0, to: 1.0)
transition.animateScale(view: iconView, from: 0.01, to: 1.0)
}
}
iconTransition.setFrame(view: iconView, frame: iconFrame)
}
if !component.isCrafting {
var buttonColor: UIColor = component.buttonColor
if let backdropAttribute = gift.gift.attributes.first(where: { attribute in
if case .backdrop = attribute {
return true
} else {
return false
}
}), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(0.65)
}
let badge: ComponentView<Empty>
var badgeTransition = transition
if let current = self.badge {
badge = current
} else {
badgeTransition = .immediate
badge = ComponentView()
self.badge = badge
}
let badgeSize = badge.update(
transition: badgeTransition,
component: AnyComponent(
ZStack([
AnyComponentWithIdentity(id: "background", component: AnyComponent(
RoundedRectangle(color: buttonColor, cornerRadius: 13.5, size: CGSize(width: 54.0, height: 27.0))
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
Text(text: "\((gift.gift.craftChancePermille ?? 0) / 10)%", font: Font.semibold(17.0), color: .white)
))
])
),
environment: {},
containerSize: CGSize(width: 54.0, height: 27.0)
)
let badgeFrame = CGRect(origin: CGPoint(x: -6.0, y: -6.0 - UIScreenPixel), size: badgeSize)
if let badgeView = badge.view {
if badgeView.superview == nil {
badgeView.isUserInteractionEnabled = false
self.backgroundView.contentView.addSubview(badgeView)
if !transition.animation.isImmediate {
transition.animateAlpha(view: badgeView, from: 0.0, to: 1.0)
transition.animateScale(view: badgeView, from: 0.01, to: 1.0)
}
}
badgeTransition.setFrame(view: badgeView, frame: badgeFrame)
}
if let _ = component.removeAction {
let removeButton: ComponentView<Empty>
var removeButtonTransition = transition
if let current = self.removeIcon {
removeButton = current
} else {
removeButtonTransition = .immediate
removeButton = ComponentView()
self.removeIcon = removeButton
}
let removeButtonSize = removeButton.update(
transition: removeButtonTransition,
component: AnyComponent(
ZStack([
AnyComponentWithIdentity(id: "background", component: AnyComponent(
RoundedRectangle(color: buttonColor, cornerRadius: 13.5, size: CGSize(width: 27.0, height: 27.0))
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
BundleIconComponent(name: "Media Gallery/PictureInPictureClose", tintColor: .white)
))
])
),
environment: {},
containerSize: CGSize(width: 27.0, height: 27.0)
)
let removeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - 21.0, y: -6.0 - UIScreenPixel), size: removeButtonSize)
if let removeButtonView = removeButton.view {
if removeButtonView.superview == nil {
removeButtonView.isUserInteractionEnabled = false
self.backgroundView.contentView.addSubview(removeButtonView)
if !transition.animation.isImmediate {
transition.animateAlpha(view: removeButtonView, from: 0.0, to: 1.0)
transition.animateScale(view: removeButtonView, from: 0.01, to: 1.0)
}
}
removeButtonTransition.setFrame(view: removeButtonView, frame: removeButtonFrame)
}
}
}
}
self.isUserInteractionEnabled = !component.isCrafting
self.button.frame = CGRect(origin: .zero, size: availableSize)
self.removeButton.isUserInteractionEnabled = component.removeAction != nil
if let removeIcon = self.removeIcon?.view {
self.removeButton.frame = removeIcon.frame.insetBy(dx: -8.0, dy: -8.0)
}
return availableSize
}
}
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, state: state, environment: environment, transition: transition)
}
}
private func generateAddIcon(backgroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 46.0, height: 46.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
context.setBlendMode(.clear)
context.setStrokeColor(UIColor.clear.cgColor)
context.setLineWidth(4.0)
context.setLineCap(.round)
context.move(to: CGPoint(x: 23.0, y: 13.0))
context.addLine(to: CGPoint(x: 23.0, y: 33.0))
context.strokePath()
context.move(to: CGPoint(x: 13.0, y: 23.0))
context.addLine(to: CGPoint(x: 33.0, y: 23.0))
context.strokePath()
})
}