mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-09-24 05:10:45 +02:00
Update Ghostgram features
This commit is contained in:
+9
-9
@@ -219,7 +219,7 @@ public final class GiftCompositionComponent: Component {
|
||||
_ attribute: StarGift.UniqueGift.Attribute,
|
||||
animDuration: Double
|
||||
) {
|
||||
guard let geom = self.spinGeom, case let .model(_, file, _) = attribute else {
|
||||
guard let geom = self.spinGeom, case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = attribute else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ public final class GiftCompositionComponent: Component {
|
||||
self.decelItemHosts.removeAll()
|
||||
|
||||
for (i, attribute) in tail.reversed().enumerated() {
|
||||
guard case let .model(_, file, _) = attribute else { continue }
|
||||
guard case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = attribute else { continue }
|
||||
|
||||
let node = DefaultAnimatedStickerNodeImpl()
|
||||
node.isUserInteractionEnabled = false
|
||||
@@ -648,7 +648,7 @@ public final class GiftCompositionComponent: Component {
|
||||
|
||||
for attribute in gift.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
animationFile = file
|
||||
if !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
@@ -705,7 +705,7 @@ public final class GiftCompositionComponent: Component {
|
||||
self.previewBackdrops = backdrops
|
||||
}
|
||||
|
||||
for case let .model(_, file, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
for case let StarGift.UniqueGift.Attribute.model(_, file, _, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
self.fetchedFiles.insert(file.fileId.id)
|
||||
}
|
||||
@@ -722,7 +722,7 @@ public final class GiftCompositionComponent: Component {
|
||||
if self.previewBackdropIndex < 0 {
|
||||
self.previewBackdropIndex = 0
|
||||
}
|
||||
if case let .model(_, file, _) = self.previewModels[Int(self.previewModelIndex)] {
|
||||
if case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = self.previewModels[Int(self.previewModelIndex)] {
|
||||
animationFile = file
|
||||
component.externalState?.previewModel = self.previewModels[Int(self.previewModelIndex)]
|
||||
}
|
||||
@@ -851,12 +851,12 @@ public final class GiftCompositionComponent: Component {
|
||||
if let (previewAttributes, mainGift) = uniqueSpinContext {
|
||||
var mainModelFile: TelegramMediaFile?
|
||||
for attribute in mainGift.attributes {
|
||||
if case let .model(_, file, _) = attribute { mainModelFile = file; break }
|
||||
if case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = attribute { mainModelFile = file; break }
|
||||
}
|
||||
|
||||
var models: [StarGift.UniqueGift.Attribute] = []
|
||||
for attribute in previewAttributes {
|
||||
if case let .model(_, file, _) = attribute,
|
||||
if case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = attribute,
|
||||
file.fileId.id != mainModelFile?.fileId.id {
|
||||
models.append(attribute)
|
||||
}
|
||||
@@ -866,7 +866,7 @@ public final class GiftCompositionComponent: Component {
|
||||
return availableSize
|
||||
}
|
||||
|
||||
for case let .model(_, file, _) in models where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
for case let StarGift.UniqueGift.Attribute.model(_, file, _, _) in models where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(
|
||||
account: component.context.account,
|
||||
userLocation: .other,
|
||||
@@ -912,7 +912,7 @@ public final class GiftCompositionComponent: Component {
|
||||
} else if !nowAnimating && wasAnimating {
|
||||
var tail = Array(models.shuffled().prefix(6))
|
||||
if let mainModelFile {
|
||||
tail.append(.model(name: "", file: mainModelFile, rarity: 0))
|
||||
tail.append(.model(name: "", file: mainModelFile, rarity: .permille(0), crafted: false))
|
||||
}
|
||||
self.beginDecelerationWithQueue(
|
||||
tail: tail,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GiftCraftScreen",
|
||||
module_name = "GiftCraftScreen",
|
||||
srcs = [
|
||||
"Sources/GiftCraftScreen.swift",
|
||||
],
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+833
@@ -0,0 +1,833 @@
|
||||
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 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)
|
||||
|
||||
self.animationView.onStickerLaunch = {
|
||||
HapticFeedback().impact(.soft)
|
||||
}
|
||||
}
|
||||
|
||||
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, isClockwise in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.isFailed = true
|
||||
self.animationView.setSticker(nil, face: 0, mirror: false)
|
||||
|
||||
var availableStickers: [ComponentView<Empty>] = []
|
||||
for (id, gift) in self.selectedGifts {
|
||||
if let id = id.base as? Int, component.gifts[Int32(id)] != nil {
|
||||
availableStickers.append(gift)
|
||||
}
|
||||
}
|
||||
let wrappingCount = min(2, availableStickers.count)
|
||||
for i in 0 ..< wrappingCount {
|
||||
if let sticker = availableStickers[i].view {
|
||||
let face: Int
|
||||
if isClockwise {
|
||||
face = i + 1
|
||||
} else {
|
||||
face = 3 - i
|
||||
}
|
||||
self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
self.flipFaces = isUpsideDown
|
||||
|
||||
Queue.mainQueue().after(0.3, {
|
||||
self.failWillFinish = true
|
||||
self.component?.willFinish(false)
|
||||
|
||||
self.craftFailPlayOnce.invoke(Void())
|
||||
})
|
||||
|
||||
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, isClockwise in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isSuccess = true
|
||||
|
||||
var availableStickers: [ComponentView<Empty>] = []
|
||||
for (id, gift) in self.selectedGifts {
|
||||
if let id = id.base as? Int, component.gifts[Int32(id)] != nil {
|
||||
availableStickers.append(gift)
|
||||
}
|
||||
}
|
||||
let wrappingCount = min(2, availableStickers.count)
|
||||
for i in 0 ..< wrappingCount {
|
||||
if let sticker = availableStickers[i].view {
|
||||
let face: Int
|
||||
if isClockwise {
|
||||
face = i + 1
|
||||
} else {
|
||||
face = 3 - i
|
||||
}
|
||||
self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
CubeFaceComponent(color: component.buttonColor, cornerRadius: 28.0)
|
||||
))
|
||||
)
|
||||
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, color: component.buttonColor))
|
||||
))
|
||||
)
|
||||
}
|
||||
if self.isFailed {
|
||||
faceItems.append(
|
||||
AnyComponentWithIdentity(id: "faildial", component: AnyComponent(
|
||||
DialIndicatorComponent(
|
||||
content: AnyComponentWithIdentity(id: "gift", component: AnyComponent(
|
||||
LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "CraftFail"),
|
||||
color: .white,
|
||||
size: CGSize(width: 52.0, height: 52.0),
|
||||
playOnce: self.craftFailPlayOnce
|
||||
)
|
||||
)),
|
||||
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.0 / 100.0,
|
||||
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(
|
||||
CubeFaceComponent(color: component.buttonColor, cornerRadius: 28.0)
|
||||
))
|
||||
)
|
||||
faceItems.append(
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
|
||||
BundleIconComponent(name: "Components/CubeSide", tintColor: nil, flipVertically: index < 4 ? self.flipFaces : false)
|
||||
))
|
||||
)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.55) {
|
||||
HapticFeedback().impact(.light)
|
||||
}
|
||||
|
||||
self.anvilPlayOnce.invoke(Void())
|
||||
Queue.mainQueue().after(0.75, {
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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, 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()
|
||||
})
|
||||
}
|
||||
|
||||
private final class CubeFaceComponent: Component {
|
||||
private let color: UIColor
|
||||
private let cornerRadius: CGFloat
|
||||
|
||||
public init(color: UIColor, cornerRadius: CGFloat) {
|
||||
self.color = color
|
||||
self.cornerRadius = cornerRadius
|
||||
}
|
||||
|
||||
public static func ==(lhs: CubeFaceComponent, rhs: CubeFaceComponent) -> Bool {
|
||||
if !lhs.color.isEqual(rhs.color) {
|
||||
return false
|
||||
}
|
||||
if lhs.cornerRadius != rhs.cornerRadius {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerCurve = .continuous
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
transition.setBackgroundColor(view: view, color: self.color)
|
||||
transition.setCornerRadius(layer: view.layer, cornerRadius: self.cornerRadius)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
+792
@@ -0,0 +1,792 @@
|
||||
import UIKit
|
||||
import simd
|
||||
import Display
|
||||
|
||||
final class Transform3DView: UIView {
|
||||
override class var layerClass: AnyClass { CATransformLayer.self }
|
||||
}
|
||||
|
||||
final class PassthroughView: UIView {
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
for subview in self.subviews where !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled {
|
||||
let converted = self.convert(point, to: subview)
|
||||
if subview.point(inside: converted, with: event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
final class CubeAnimationView: UIView {
|
||||
private let cubeSize: CGFloat
|
||||
private var perspective: CGFloat = 400.0
|
||||
private let stickerSize: CGFloat
|
||||
private let stickerGap: CGFloat
|
||||
|
||||
private let camera = UIView()
|
||||
private let cubeContainer = Transform3DView()
|
||||
private var faces: [UIView] = []
|
||||
private var faceOccupants: [Int: UIView] = [:]
|
||||
|
||||
let stickerContainer = PassthroughView()
|
||||
|
||||
private var stickers: [UIView] = []
|
||||
private var isRunning = false
|
||||
|
||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||
private var lastTimestamp: CFTimeInterval = 0
|
||||
private var warpDisplayLink: SharedDisplayLinkDriver.Link?
|
||||
private weak var warpView: UIView?
|
||||
private var warpStartQuad: Quad?
|
||||
private var warpEndQuad: Quad?
|
||||
private var warpDuration: TimeInterval = 0
|
||||
private var warpDynamicTarget: (() -> Quad)?
|
||||
private var warpCompletion: (() -> Void)?
|
||||
private var warpStartTimestamp: CFTimeInterval = 0
|
||||
private var warpLastProgress: CGFloat = 0
|
||||
private var warpCurrentQuad: Quad?
|
||||
private var warpHasCompleted = false
|
||||
private var warpSnapshot: UIView?
|
||||
|
||||
private var rotation = SIMD3<Float>(repeating: 0)
|
||||
private var angularVelocity = SIMD3<Float>(repeating: 0)
|
||||
|
||||
private let dampingPerSecond: Float = 0.66
|
||||
private let finishSpringX: Float = 28.0
|
||||
private let finishSpringY: Float = 18.0
|
||||
private let finishDampingX: Float = 2.0 * sqrt(28.0)
|
||||
private let finishDampingY: Float = 2.0 * sqrt(18.0)
|
||||
private let finishWobbleAmplitudeZ: Float = 10.0 * .pi / 180.0
|
||||
private let finishWobbleCycles: Float = 1.0
|
||||
private let finishWobbleDampingExponent: Float = 0.6
|
||||
private let finishSuccessScale: Float = 1.3
|
||||
private let finishSuccessScaleTriggerAngle: Float = 0.4 * .pi
|
||||
private let finishApproachTriggerAngle: Float = 1.5 * .pi
|
||||
private let baseImpulseStrength: Float = 4.0
|
||||
private let impactNudgeDistance: CGFloat = 20.0
|
||||
private let impactNudgeEmphasis: CGFloat = 28.0
|
||||
|
||||
private var isFinishingX = false
|
||||
private var isFinishingY = false
|
||||
private var finishTargetX: Float = 0.0
|
||||
private var finishTargetY: Float = 0.0
|
||||
private var finishDirectionY: Float = 1.0
|
||||
private var finishRotationY: Float = 0.0
|
||||
private var finishTargetYUnwrapped: Float = 0.0
|
||||
private var finishRemainingYStart: Float = 0.0
|
||||
private var finishDelayTimerX: Timer?
|
||||
private var finishDelayTimerY: Timer?
|
||||
private var cubeScale: Float = 1.0
|
||||
private var hasFiredFinishApproach = false
|
||||
|
||||
var isSuccess = false
|
||||
|
||||
var onStickerLaunch: (() -> Void)?
|
||||
var onFinishApproach: ((Bool, Bool) -> Void)?
|
||||
|
||||
private let defaultStickOrder: [Int] = [0, 5, 4, 3]
|
||||
private let sequenceStickOrders: [String: [Int]] = [
|
||||
"0": [0],
|
||||
"0,1": [0, 5],
|
||||
"0,2": [0, 5],
|
||||
"0,3": [0, 5],
|
||||
"0,1,2": [0, 5, 4],
|
||||
"0,1,3": [0, 5, 2],
|
||||
"0,2,3": [0, 5, 1],
|
||||
"0,1,2,3": [0, 5, 4, 3]
|
||||
]
|
||||
private var activeStickOrder: [Int] = []
|
||||
|
||||
init(cubeSize: CGFloat = 110.0, stickerSize: CGFloat = 76.0, stickerGap: CGFloat = 30.0) {
|
||||
self.cubeSize = cubeSize
|
||||
self.stickerSize = stickerSize
|
||||
self.stickerGap = stickerGap
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.activeStickOrder = self.defaultStickOrder
|
||||
|
||||
self.camera.backgroundColor = .clear
|
||||
self.camera.clipsToBounds = false
|
||||
self.addSubview(self.camera)
|
||||
|
||||
self.cubeContainer.backgroundColor = .clear
|
||||
self.cubeContainer.clipsToBounds = false
|
||||
self.camera.addSubview(self.cubeContainer)
|
||||
|
||||
var p = CATransform3DIdentity
|
||||
p.m34 = -1.0 / self.perspective
|
||||
self.camera.layer.sublayerTransform = p
|
||||
self.stickerContainer.layer.sublayerTransform = p
|
||||
|
||||
self.stickerContainer.backgroundColor = .clear
|
||||
self.stickerContainer.clipsToBounds = false
|
||||
self.addSubview(self.stickerContainer)
|
||||
|
||||
#if DEBUG
|
||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
||||
self.camera.addGestureRecognizer(pan)
|
||||
#endif
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.camera.bounds = CGRect(x: 0, y: 0, width: self.cubeSize, height: self.cubeSize)
|
||||
self.camera.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
|
||||
|
||||
self.cubeContainer.frame = self.camera.bounds
|
||||
self.stickerContainer.frame = self.bounds
|
||||
|
||||
self.layoutStickers()
|
||||
self.layoutFaces()
|
||||
self.applyCubeRotation()
|
||||
}
|
||||
|
||||
func setStickers(_ views: [UIView]) {
|
||||
self.stickers = views
|
||||
for view in views {
|
||||
view.layer.anchorPoint = .zero
|
||||
view.isUserInteractionEnabled = true
|
||||
|
||||
if view.superview !== self.stickerContainer {
|
||||
self.stickerContainer.addSubview(view)
|
||||
}
|
||||
}
|
||||
self.layoutStickers()
|
||||
}
|
||||
|
||||
func setSticker(_ sticker: UIView?, face index: Int, mirror: Bool, animated: Bool = false) {
|
||||
guard self.faces.indices.contains(index) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let existing = self.faceOccupants[index] {
|
||||
existing.removeFromSuperview()
|
||||
self.faceOccupants[index] = nil
|
||||
}
|
||||
|
||||
guard let sticker else {
|
||||
return
|
||||
}
|
||||
|
||||
if let priorIndex = self.faceOccupants.first(where: { $0.value === sticker })?.key {
|
||||
self.faceOccupants[priorIndex] = nil
|
||||
}
|
||||
|
||||
if animated, let stickerSuperview = sticker.superview, let snapshotView = sticker.snapshotView(afterScreenUpdates: false) {
|
||||
stickerSuperview.addSubview(snapshotView)
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
sticker.removeFromSuperview()
|
||||
|
||||
let targetFace = self.faces[index]
|
||||
targetFace.addSubview(sticker)
|
||||
self.faceOccupants[index] = sticker
|
||||
|
||||
sticker.layer.removeAllAnimations()
|
||||
sticker.transform = .identity
|
||||
sticker.layer.transform = CATransform3DIdentity
|
||||
sticker.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
sticker.layer.isDoubleSided = false
|
||||
sticker.clipsToBounds = false
|
||||
sticker.isUserInteractionEnabled = false
|
||||
|
||||
let faceStickerSize = self.cubeSize
|
||||
sticker.bounds = CGRect(x: 0, y: 0, width: faceStickerSize, height: faceStickerSize)
|
||||
sticker.center = CGPoint(x: self.cubeSize / 2, y: self.cubeSize / 2)
|
||||
|
||||
var snappedAngle: CGFloat = 0.0
|
||||
if mirror {
|
||||
snappedAngle += .pi
|
||||
}
|
||||
sticker.transform = CGAffineTransform(rotationAngle: snappedAngle)
|
||||
|
||||
if animated {
|
||||
sticker.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
func startStickerSequence(indices: [Int]? = nil) {
|
||||
guard !self.isRunning else {
|
||||
return
|
||||
}
|
||||
guard self.stickers.contains(where: { $0.superview === self.stickerContainer }) else {
|
||||
return
|
||||
}
|
||||
self.isRunning = true
|
||||
|
||||
let sequence: [Int]
|
||||
if let indices, !indices.isEmpty {
|
||||
var seen = Set<Int>()
|
||||
var result: [Int] = []
|
||||
for index in indices where self.stickers.indices.contains(index) {
|
||||
if seen.insert(index).inserted {
|
||||
result.append(index)
|
||||
}
|
||||
}
|
||||
sequence = result
|
||||
} else {
|
||||
sequence = Array(self.stickers.indices)
|
||||
}
|
||||
|
||||
var stickOrder: [Int]
|
||||
let key = sequence.map(String.init).joined(separator: ",")
|
||||
if let order = self.sequenceStickOrders[key] {
|
||||
stickOrder = order
|
||||
} else {
|
||||
stickOrder = Array(self.defaultStickOrder.prefix(sequence.count))
|
||||
}
|
||||
self.activeStickOrder = stickOrder
|
||||
|
||||
self.scheduleStickerSequence(from: 0, indices: sequence)
|
||||
}
|
||||
|
||||
func resetAll() {
|
||||
self.isRunning = false
|
||||
self.resetStickers()
|
||||
self.resetCube()
|
||||
self.activeStickOrder = self.defaultStickOrder
|
||||
}
|
||||
|
||||
func setFaces(_ views: [UIView]) {
|
||||
guard views.count == 6 else {
|
||||
return
|
||||
}
|
||||
self.faces.forEach { $0.removeFromSuperview() }
|
||||
self.faces = views
|
||||
for face in views {
|
||||
face.layer.isDoubleSided = false
|
||||
self.cubeContainer.addSubview(face)
|
||||
}
|
||||
self.layoutFaces()
|
||||
}
|
||||
|
||||
private func layoutFaces() {
|
||||
guard self.faces.count == 6 else {
|
||||
return
|
||||
}
|
||||
let half = self.cubeSize / 2
|
||||
|
||||
for face in self.faces {
|
||||
face.bounds = CGRect(x: 0, y: 0, width: self.cubeSize, height: self.cubeSize)
|
||||
face.center = CGPoint(x: self.cubeSize / 2, y: self.cubeSize / 2)
|
||||
}
|
||||
|
||||
func faceTransform(rx: CGFloat, ry: CGFloat) -> CATransform3D {
|
||||
var m = CATransform3DIdentity
|
||||
m = CATransform3DRotate(m, rx, 1, 0, 0)
|
||||
m = CATransform3DRotate(m, ry, 0, 1, 0)
|
||||
m = CATransform3DTranslate(m, 0, 0, half)
|
||||
return m
|
||||
}
|
||||
|
||||
self.faces[0].layer.transform = faceTransform(rx: 0, ry: 0)
|
||||
self.faces[1].layer.transform = faceTransform(rx: 0, ry: .pi / 2)
|
||||
self.faces[2].layer.transform = faceTransform(rx: 0, ry: .pi)
|
||||
self.faces[3].layer.transform = faceTransform(rx: 0, ry: -.pi / 2)
|
||||
self.faces[4].layer.transform = faceTransform(rx: -.pi / 2, ry: 0)
|
||||
self.faces[5].layer.transform = faceTransform(rx: .pi / 2, ry: 0)
|
||||
}
|
||||
|
||||
private func animateWarp(for view: UIView, from startQuad: Quad, to targetQuad: Quad, duration: TimeInterval, dynamicTarget: (() -> Quad)? = nil, completion: @escaping () -> Void) {
|
||||
self.cancelWarp()
|
||||
self.warpView = view
|
||||
self.warpStartQuad = startQuad
|
||||
self.warpEndQuad = targetQuad
|
||||
self.warpDuration = duration
|
||||
self.warpDynamicTarget = dynamicTarget
|
||||
self.warpCompletion = completion
|
||||
self.warpStartTimestamp = 0
|
||||
self.warpLastProgress = 0
|
||||
self.warpHasCompleted = false
|
||||
self.warpCurrentQuad = startQuad
|
||||
startQuad.apply(to: view)
|
||||
|
||||
let link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max) { [weak self] _ in
|
||||
self?.stepWarp()
|
||||
}
|
||||
link.isPaused = false
|
||||
self.warpDisplayLink = link
|
||||
}
|
||||
|
||||
private func stepWarp() {
|
||||
guard let view = self.warpView, let currentQuad = self.warpCurrentQuad, let endQuad = self.warpEndQuad else {
|
||||
self.finishWarp()
|
||||
return
|
||||
}
|
||||
|
||||
if self.warpStartTimestamp == 0 {
|
||||
self.warpStartTimestamp = CACurrentMediaTime()
|
||||
}
|
||||
|
||||
let elapsed = CACurrentMediaTime() - self.warpStartTimestamp
|
||||
let progress = self.warpDuration > 0 ? min(1.0, elapsed / self.warpDuration) : 1.0
|
||||
let t = CGFloat(progress)
|
||||
let eased = t * t * (3 - 2 * t)
|
||||
let target = self.warpDynamicTarget?() ?? endQuad
|
||||
let delta = eased - self.warpLastProgress
|
||||
let remaining = max(1 - self.warpLastProgress, 0.0001)
|
||||
let weight = max(0, min(1, delta / remaining))
|
||||
let nextQuad = currentQuad.interpolated(to: target, t: weight)
|
||||
nextQuad.apply(to: view)
|
||||
self.warpCurrentQuad = nextQuad
|
||||
self.warpLastProgress = eased
|
||||
|
||||
if progress >= 1.0 {
|
||||
self.finishWarp()
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelWarp() {
|
||||
self.warpHasCompleted = true
|
||||
self.warpDisplayLink?.invalidate()
|
||||
self.warpDisplayLink = nil
|
||||
self.warpCompletion = nil
|
||||
self.clearWarpState()
|
||||
}
|
||||
|
||||
private func finishWarp() {
|
||||
guard !self.warpHasCompleted else { return }
|
||||
self.warpHasCompleted = true
|
||||
self.warpDisplayLink?.invalidate()
|
||||
self.warpDisplayLink = nil
|
||||
self.warpCompletion?()
|
||||
self.warpCompletion = nil
|
||||
self.clearWarpState()
|
||||
}
|
||||
|
||||
private func clearWarpState() {
|
||||
self.warpView = nil
|
||||
self.warpStartQuad = nil
|
||||
self.warpEndQuad = nil
|
||||
self.warpDynamicTarget = nil
|
||||
self.warpStartTimestamp = 0
|
||||
self.warpLastProgress = 0
|
||||
self.warpCurrentQuad = nil
|
||||
}
|
||||
|
||||
private func projectedQuad(for face: UIView) -> ProjectedFace {
|
||||
let bounds = face.bounds
|
||||
|
||||
func project(_ p: CGPoint) -> CGPoint {
|
||||
let inRoot = face.layer.convert(p, to: self.layer)
|
||||
return self.stickerContainer.layer.convert(inRoot, from: self.layer)
|
||||
}
|
||||
|
||||
var topLeft = project(CGPoint(x: bounds.minX, y: bounds.minY))
|
||||
var topRight = project(CGPoint(x: bounds.maxX, y: bounds.minY))
|
||||
var bottomLeft = project(CGPoint(x: bounds.minX, y: bounds.maxY))
|
||||
var bottomRight = project(CGPoint(x: bounds.maxX, y: bounds.maxY))
|
||||
|
||||
func center(_ a: CGPoint, _ b: CGPoint) -> CGPoint {
|
||||
CGPoint(x: (a.x + b.x) * 0.5, y: (a.y + b.y) * 0.5)
|
||||
}
|
||||
|
||||
func normalized(_ v: CGPoint) -> CGPoint? {
|
||||
let len = hypot(v.x, v.y)
|
||||
guard len > 1e-5 else { return nil }
|
||||
return CGPoint(x: v.x / len, y: v.y / len)
|
||||
}
|
||||
|
||||
func dot(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
|
||||
a.x * b.x + a.y * b.y
|
||||
}
|
||||
|
||||
let screenUp = CGPoint(x: 0, y: -1)
|
||||
let screenRight = CGPoint(x: 1, y: 0)
|
||||
|
||||
if let up = normalized(CGPoint(
|
||||
x: center(topLeft, topRight).x - center(bottomLeft, bottomRight).x,
|
||||
y: center(topLeft, topRight).y - center(bottomLeft, bottomRight).y
|
||||
)), dot(up, screenUp) < 0 {
|
||||
swap(&topLeft, &bottomLeft)
|
||||
swap(&topRight, &bottomRight)
|
||||
}
|
||||
|
||||
let faceOrigin = project(.zero)
|
||||
let faceX = project(CGPoint(x: 1, y: 0))
|
||||
|
||||
if let right = normalized(CGPoint(
|
||||
x: center(topRight, bottomRight).x - center(topLeft, bottomLeft).x,
|
||||
y: center(topRight, bottomRight).y - center(topLeft, bottomLeft).y
|
||||
)), dot(right, screenRight) < 0 {
|
||||
swap(&topLeft, &topRight)
|
||||
swap(&bottomLeft, &bottomRight)
|
||||
}
|
||||
|
||||
let quad = Quad(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
|
||||
|
||||
let desiredTopVector = CGPoint(x: quad.topRight.x - quad.topLeft.x, y: quad.topRight.y - quad.topLeft.y)
|
||||
let baseTopVector = CGPoint(x: faceX.x - faceOrigin.x, y: faceX.y - faceOrigin.y)
|
||||
|
||||
let desiredAngle = atan2(desiredTopVector.y, desiredTopVector.x)
|
||||
let baseAngle = atan2(baseTopVector.y, baseTopVector.x)
|
||||
let rotation = normalizeAngle(desiredAngle - baseAngle)
|
||||
|
||||
return ProjectedFace(quad: quad, rotation: rotation)
|
||||
}
|
||||
|
||||
private func layoutStickers() {
|
||||
guard !self.stickers.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let cubeCenterInSticker = self.stickerContainer.convert(self.camera.center, from: self)
|
||||
let r = self.cubeSize / 2 + self.stickerGap + self.stickerSize / 2
|
||||
let scale = self.stickerSize / self.cubeSize
|
||||
|
||||
let positions = [
|
||||
CGPoint(x: cubeCenterInSticker.x - r, y: cubeCenterInSticker.y - r * 0.4),
|
||||
CGPoint(x: cubeCenterInSticker.x + r, y: cubeCenterInSticker.y - r * 0.4),
|
||||
CGPoint(x: cubeCenterInSticker.x - r, y: cubeCenterInSticker.y + r * 0.4),
|
||||
CGPoint(x: cubeCenterInSticker.x + r, y: cubeCenterInSticker.y + r * 0.4)
|
||||
]
|
||||
|
||||
for (i, view) in self.stickers.enumerated() {
|
||||
if view.superview !== self.stickerContainer {
|
||||
continue
|
||||
}
|
||||
view.bounds = CGRect(x: 0, y: 0, width: self.cubeSize, height: self.cubeSize)
|
||||
view.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
view.center = CGPoint(x: positions[i].x - self.stickerSize * 0.5, y: positions[i].y - self.stickerSize * 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||||
let translation = gesture.translation(in: self.camera)
|
||||
switch gesture.state {
|
||||
case .changed:
|
||||
let delta = CGPoint(x: translation.x, y: translation.y)
|
||||
|
||||
self.rotation.y += Float(delta.x) * 0.018
|
||||
self.rotation.x += Float(-delta.y) * 0.018
|
||||
self.rotation = normalizedRotation(self.rotation)
|
||||
self.applyCubeRotation()
|
||||
|
||||
gesture.setTranslation(.zero, in: self.camera)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func launchStickerView(_ sticker: UIView, emphasized: Bool, willFinish: Bool = false) {
|
||||
guard sticker.superview === self.stickerContainer else {
|
||||
return
|
||||
}
|
||||
var number = 0
|
||||
if self.faceOccupants.count < self.activeStickOrder.count {
|
||||
number = self.activeStickOrder[self.faceOccupants.count]
|
||||
}
|
||||
let faceIndex = number
|
||||
guard self.faces.count > faceIndex else { return }
|
||||
let targetFace = self.faces[faceIndex]
|
||||
|
||||
let startCenterInSticker = sticker.center
|
||||
let cubeCenterInSticker = self.stickerContainer.convert(self.camera.center, from: self)
|
||||
|
||||
sticker.isUserInteractionEnabled = false
|
||||
sticker.layer.isDoubleSided = false
|
||||
|
||||
let faceStickerSize = self.cubeSize
|
||||
let duration: TimeInterval = 0.2
|
||||
let startQuad = Quad(rect: sticker.frame)
|
||||
let animationView: UIView
|
||||
if let snapshot = sticker.snapshotView(afterScreenUpdates: false) {
|
||||
self.warpSnapshot?.removeFromSuperview()
|
||||
self.warpSnapshot = snapshot
|
||||
|
||||
snapshot.bounds = sticker.bounds
|
||||
snapshot.center = sticker.center
|
||||
snapshot.layer.anchorPoint = sticker.layer.anchorPoint
|
||||
snapshot.layer.transform = sticker.layer.transform
|
||||
snapshot.layer.isDoubleSided = sticker.layer.isDoubleSided
|
||||
snapshot.isUserInteractionEnabled = false
|
||||
self.stickerContainer.addSubview(snapshot)
|
||||
|
||||
sticker.isHidden = true
|
||||
animationView = snapshot
|
||||
} else {
|
||||
animationView = sticker
|
||||
}
|
||||
sticker.transform = .identity
|
||||
|
||||
let projectedFace = self.projectedQuad(for: targetFace)
|
||||
let targetQuad = projectedFace.quad
|
||||
let dynamicTarget: () -> Quad = { [weak self, weak targetFace] in
|
||||
guard let self, let face = targetFace else {
|
||||
return targetQuad
|
||||
}
|
||||
return self.projectedQuad(for: face).quad
|
||||
}
|
||||
|
||||
self.animateWarp(for: animationView, from: startQuad, to: targetQuad, duration: duration, dynamicTarget: dynamicTarget) { [weak self, weak sticker, weak targetFace, weak animationView] in
|
||||
guard let self, let sticker, let targetFace else {
|
||||
return
|
||||
}
|
||||
|
||||
self.onStickerLaunch?()
|
||||
|
||||
if let animationView, animationView !== sticker {
|
||||
animationView.removeFromSuperview()
|
||||
self.warpSnapshot = nil
|
||||
sticker.isHidden = false
|
||||
}
|
||||
|
||||
sticker.removeFromSuperview()
|
||||
targetFace.addSubview(sticker)
|
||||
self.faceOccupants[faceIndex] = sticker
|
||||
|
||||
sticker.bounds = CGRect(x: 0, y: 0, width: faceStickerSize, height: faceStickerSize)
|
||||
sticker.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
sticker.center = CGPoint(x: self.cubeSize / 2, y: self.cubeSize / 2)
|
||||
sticker.layer.transform = CATransform3DIdentity
|
||||
let finalProjection = self.projectedQuad(for: targetFace)
|
||||
let snappedAngle = snappedRightAngle(finalProjection.rotation)
|
||||
sticker.transform = CGAffineTransform(rotationAngle: snappedAngle)
|
||||
|
||||
let delta = SIMD2<Float>(Float(cubeCenterInSticker.x - startCenterInSticker.x), Float(cubeCenterInSticker.y - startCenterInSticker.y))
|
||||
let direction = normalize2(delta)
|
||||
self.applyImpulse(direction: direction, emphasized: emphasized, replace: true)
|
||||
self.applyImpactSpring(direction: direction, emphasized: emphasized)
|
||||
if willFinish {
|
||||
self.startFinishingAnimation()
|
||||
}
|
||||
self.startSpinLoopIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func resetStickers() {
|
||||
self.cancelWarp()
|
||||
self.warpSnapshot?.removeFromSuperview()
|
||||
self.warpSnapshot = nil
|
||||
|
||||
for sticker in self.stickers {
|
||||
sticker.layer.removeAllAnimations()
|
||||
sticker.transform = .identity
|
||||
sticker.layer.transform = CATransform3DIdentity
|
||||
sticker.layer.anchorPoint = .zero
|
||||
sticker.layer.isDoubleSided = true
|
||||
sticker.clipsToBounds = false
|
||||
sticker.isUserInteractionEnabled = true
|
||||
sticker.removeFromSuperview()
|
||||
self.stickerContainer.addSubview(sticker)
|
||||
}
|
||||
|
||||
self.faceOccupants.removeAll()
|
||||
self.layoutStickers()
|
||||
}
|
||||
|
||||
private func resetCube() {
|
||||
self.displayLink?.invalidate()
|
||||
self.displayLink = nil
|
||||
self.angularVelocity = .zero
|
||||
self.lastTimestamp = 0
|
||||
self.isFinishingX = false
|
||||
self.isFinishingY = false
|
||||
self.finishDelayTimerX?.invalidate()
|
||||
self.finishDelayTimerX = nil
|
||||
self.finishDelayTimerY?.invalidate()
|
||||
self.finishDelayTimerY = nil
|
||||
self.cubeScale = 1.0
|
||||
self.hasFiredFinishApproach = false
|
||||
|
||||
self.rotation = SIMD3<Float>(repeating: 0)
|
||||
self.cubeScale = 1.0
|
||||
self.applyCubeRotation()
|
||||
}
|
||||
|
||||
private func scheduleStickerSequence(from index: Int, indices: [Int]) {
|
||||
guard self.isRunning else {
|
||||
return
|
||||
}
|
||||
guard index < indices.count else {
|
||||
self.isRunning = false
|
||||
return
|
||||
}
|
||||
|
||||
let delay: TimeInterval = index == 0 ? 0.0 : 1.0
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard self.isRunning else {
|
||||
return
|
||||
}
|
||||
let stickerIndex = indices[index]
|
||||
if self.stickers.indices.contains(stickerIndex) {
|
||||
let isLast = index == indices.count - 1
|
||||
self.launchStickerView(self.stickers[stickerIndex], emphasized: isLast, willFinish: isLast)
|
||||
}
|
||||
self.scheduleStickerSequence(from: index + 1, indices: indices)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyImpulse(direction: SIMD2<Float>, emphasized: Bool, replace: Bool) {
|
||||
var xStrength = self.baseImpulseStrength
|
||||
var yStrength = self.baseImpulseStrength
|
||||
if emphasized {
|
||||
xStrength *= 10.0
|
||||
yStrength *= 4.0
|
||||
}
|
||||
let impulseX: Float = -direction.y * xStrength
|
||||
let impulseY: Float = direction.x * yStrength
|
||||
let impulseZ: Float = 0.0
|
||||
|
||||
if replace {
|
||||
self.angularVelocity = SIMD3<Float>(impulseX, impulseY, impulseZ)
|
||||
} else {
|
||||
self.angularVelocity += SIMD3<Float>(impulseX, impulseY, impulseZ)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyImpactSpring(direction: SIMD2<Float>, emphasized: Bool) {
|
||||
guard simd_length(direction) > 0.0001 else {
|
||||
return
|
||||
}
|
||||
let distance = emphasized ? self.impactNudgeEmphasis : self.impactNudgeDistance
|
||||
let offsetX = CGFloat(direction.x) * distance
|
||||
let offsetY = CGFloat(direction.y) * distance
|
||||
|
||||
let currentTransform = self.camera.layer.presentation()?.affineTransform() ?? self.camera.transform
|
||||
self.camera.layer.removeAllAnimations()
|
||||
let impactTransform = currentTransform.translatedBy(x: offsetX, y: offsetY)
|
||||
|
||||
UIView.animate(withDuration: 0.08, delay: 0.0, options: [.curveEaseOut, .beginFromCurrentState]) {
|
||||
self.camera.transform = impactTransform
|
||||
} completion: { _ in
|
||||
UIView.animate(withDuration: 0.55, delay: 0, usingSpringWithDamping: 0.72, initialSpringVelocity: 0.2, options: .beginFromCurrentState) {
|
||||
self.camera.transform = .identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startSpinLoopIfNeeded() {
|
||||
if self.displayLink == nil {
|
||||
let link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max) { [weak self] _ in
|
||||
self?.tick()
|
||||
}
|
||||
link.isPaused = false
|
||||
self.displayLink = link
|
||||
self.lastTimestamp = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
private func tick() {
|
||||
let ts = CACurrentMediaTime()
|
||||
if self.lastTimestamp == 0 { self.lastTimestamp = ts; return }
|
||||
let dt = Float(ts - self.lastTimestamp)
|
||||
self.lastTimestamp = ts
|
||||
|
||||
self.rotation += self.angularVelocity * dt
|
||||
if self.isFinishingX {
|
||||
let delta = shortestAngleDelta(from: self.rotation.x, to: self.finishTargetX)
|
||||
let accel = self.finishSpringX * delta - self.finishDampingX * self.angularVelocity.x
|
||||
self.angularVelocity.x += accel * dt
|
||||
if abs(delta) < 0.0006 && abs(self.angularVelocity.x) < 0.001 {
|
||||
self.rotation.x = self.finishTargetX
|
||||
self.angularVelocity.x = 0.0
|
||||
self.isFinishingX = false
|
||||
}
|
||||
}
|
||||
if self.isFinishingY {
|
||||
self.finishRotationY += self.angularVelocity.y * dt
|
||||
let remaining = self.finishTargetYUnwrapped - self.finishRotationY
|
||||
let accel = self.finishSpringY * remaining - self.finishDampingY * self.angularVelocity.y
|
||||
self.angularVelocity.y += accel * dt
|
||||
self.rotation.y = normalizeAngle(self.finishRotationY)
|
||||
let total = max(abs(self.finishRemainingYStart), 0.0001)
|
||||
let progress = min(max(1.0 - abs(remaining) / total, 0.0), 1.0)
|
||||
let damping = pow(1.0 - progress, self.finishWobbleDampingExponent)
|
||||
let phase = 2.0 * Float.pi * self.finishWobbleCycles * progress
|
||||
self.rotation.z = self.finishWobbleAmplitudeZ * sin(phase) * damping
|
||||
let absRemaining = abs(remaining)
|
||||
if !self.hasFiredFinishApproach && absRemaining <= self.finishApproachTriggerAngle {
|
||||
self.hasFiredFinishApproach = true
|
||||
let upsideDown = abs(shortestAngleDelta(from: self.rotation.x, to: Float.pi)) < (Float.pi / 2)
|
||||
let isClockwise = self.finishDirectionY > 0
|
||||
self.onFinishApproach?(upsideDown, isClockwise)
|
||||
}
|
||||
if self.isSuccess, absRemaining <= self.finishSuccessScaleTriggerAngle {
|
||||
let raw = (self.finishSuccessScaleTriggerAngle - absRemaining) / self.finishSuccessScaleTriggerAngle
|
||||
let eased = raw * raw * (3 - 2 * raw)
|
||||
self.cubeScale = 1.0 + (self.finishSuccessScale - 1.0) * eased
|
||||
} else if !self.isSuccess {
|
||||
self.cubeScale = 1.0
|
||||
}
|
||||
if abs(remaining) < 0.0008 && abs(self.angularVelocity.y) < 0.0015 {
|
||||
self.finishRotationY = self.finishTargetYUnwrapped
|
||||
self.rotation.y = self.finishTargetY
|
||||
self.angularVelocity.y = 0.0
|
||||
self.isFinishingY = false
|
||||
self.rotation.z = 0.0
|
||||
self.angularVelocity.z = 0.0
|
||||
}
|
||||
} else if self.rotation.z != 0 {
|
||||
self.rotation.z = 0.0
|
||||
}
|
||||
self.rotation = normalizedRotation(self.rotation)
|
||||
|
||||
let damp = pow(self.dampingPerSecond, dt)
|
||||
self.angularVelocity *= damp
|
||||
|
||||
self.applyCubeRotation()
|
||||
}
|
||||
|
||||
private func startFinishingAnimation() {
|
||||
self.finishDelayTimerX?.invalidate()
|
||||
self.finishDelayTimerX = Timer.scheduledTimer(withTimeInterval: 0.75, repeats: false) { [weak self] _ in
|
||||
self?.beginFinishingX()
|
||||
}
|
||||
}
|
||||
|
||||
private func beginFinishingX() {
|
||||
let deltaToZero = abs(shortestAngleDelta(from: self.rotation.x, to: 0))
|
||||
let deltaToPi = abs(shortestAngleDelta(from: self.rotation.x, to: Float.pi))
|
||||
self.finishTargetX = deltaToZero <= deltaToPi ? 0 : Float.pi
|
||||
self.finishTargetY = self.finishTargetX == 0 ? 0 : Float.pi
|
||||
self.isFinishingX = true
|
||||
self.finishDelayTimerY?.invalidate()
|
||||
self.finishDelayTimerY = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||
self?.beginFinishingY()
|
||||
}
|
||||
}
|
||||
|
||||
private func beginFinishingY() {
|
||||
self.finishRotationY = self.rotation.y
|
||||
let directionY = nonZeroSign(self.angularVelocity.y, fallback: 1)
|
||||
self.finishDirectionY = directionY
|
||||
let startMod = normalizeAnglePositive(self.finishRotationY)
|
||||
let targetMod = normalizeAnglePositive(self.finishTargetY)
|
||||
let baseDelta: Float
|
||||
if directionY >= 0 {
|
||||
baseDelta = targetMod >= startMod ? targetMod - startMod : (Float.pi * 2) - (startMod - targetMod)
|
||||
} else {
|
||||
baseDelta = startMod >= targetMod ? startMod - targetMod : (Float.pi * 2) - (targetMod - startMod)
|
||||
}
|
||||
var delta = baseDelta
|
||||
if delta < Float.pi {
|
||||
delta += Float.pi * 2
|
||||
}
|
||||
self.finishTargetYUnwrapped = self.finishRotationY + directionY * delta
|
||||
self.finishRemainingYStart = self.finishTargetYUnwrapped - self.finishRotationY
|
||||
self.isFinishingY = true
|
||||
self.hasFiredFinishApproach = false
|
||||
}
|
||||
|
||||
private func applyCubeRotation() {
|
||||
var m = CATransform3DIdentity
|
||||
m = CATransform3DRotate(m, CGFloat(self.rotation.x), 1, 0, 0)
|
||||
m = CATransform3DRotate(m, CGFloat(self.rotation.y), 0, 1, 0)
|
||||
m = CATransform3DRotate(m, CGFloat(self.rotation.z), 0, 0, 1)
|
||||
m = CATransform3DScale(m, CGFloat(self.cubeScale), CGFloat(self.cubeScale), 1)
|
||||
self.cubeContainer.layer.transform = m
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import UIKit
|
||||
import simd
|
||||
|
||||
func normalize2(_ v: SIMD2<Float>) -> SIMD2<Float> {
|
||||
let l = simd_length(v)
|
||||
return l > 1e-5 ? v / l : SIMD2<Float>(0, 0)
|
||||
}
|
||||
|
||||
func normalizedRotation(_ r: SIMD3<Float>) -> SIMD3<Float> {
|
||||
SIMD3<Float>(normalizeAngle(r.x), normalizeAngle(r.y), normalizeAngle(r.z))
|
||||
}
|
||||
|
||||
struct ProjectedFace {
|
||||
let quad: Quad
|
||||
let rotation: CGFloat
|
||||
}
|
||||
|
||||
struct Quad {
|
||||
var topLeft: CGPoint
|
||||
var topRight: CGPoint
|
||||
var bottomLeft: CGPoint
|
||||
var bottomRight: CGPoint
|
||||
|
||||
init(topLeft: CGPoint, topRight: CGPoint, bottomLeft: CGPoint, bottomRight: CGPoint) {
|
||||
self.topLeft = topLeft
|
||||
self.topRight = topRight
|
||||
self.bottomLeft = bottomLeft
|
||||
self.bottomRight = bottomRight
|
||||
}
|
||||
|
||||
init(rect: CGRect) {
|
||||
self.init(
|
||||
topLeft: rect.origin,
|
||||
topRight: CGPoint(x: rect.maxX, y: rect.minY),
|
||||
bottomLeft: CGPoint(x: rect.minX, y: rect.maxY),
|
||||
bottomRight: CGPoint(x: rect.maxX, y: rect.maxY)
|
||||
)
|
||||
}
|
||||
|
||||
func boundingBox() -> CGRect {
|
||||
let xs = [topLeft.x, topRight.x, bottomLeft.x, bottomRight.x]
|
||||
let ys = [topLeft.y, topRight.y, bottomLeft.y, bottomRight.y]
|
||||
guard let minX = xs.min(), let maxX = xs.max(), let minY = ys.min(), let maxY = ys.max() else {
|
||||
return .zero
|
||||
}
|
||||
return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
|
||||
}
|
||||
|
||||
func offsetting(dx: CGFloat, dy: CGFloat) -> Quad {
|
||||
return Quad(
|
||||
topLeft: CGPoint(x: topLeft.x + dx, y: topLeft.y + dy),
|
||||
topRight: CGPoint(x: topRight.x + dx, y: topRight.y + dy),
|
||||
bottomLeft: CGPoint(x: bottomLeft.x + dx, y: bottomLeft.y + dy),
|
||||
bottomRight: CGPoint(x: bottomRight.x + dx, y: bottomRight.y + dy)
|
||||
)
|
||||
}
|
||||
|
||||
func interpolated(to other: Quad, t: CGFloat) -> Quad {
|
||||
return Quad(
|
||||
topLeft: lerp(topLeft, other.topLeft, t),
|
||||
topRight: lerp(topRight, other.topRight, t),
|
||||
bottomLeft: lerp(bottomLeft, other.bottomLeft, t),
|
||||
bottomRight: lerp(bottomRight, other.bottomRight, t)
|
||||
)
|
||||
}
|
||||
|
||||
func apply(to view: UIView) {
|
||||
let bounds = boundingBox()
|
||||
let localQuad = offsetting(dx: -bounds.origin.x, dy: -bounds.origin.y)
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
view.frame = bounds
|
||||
let transform = rectToQuad(rect: view.bounds, quad: localQuad)
|
||||
view.layer.transform = transform
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
func lerp(_ a: CGFloat, _ b: CGFloat, _ t: CGFloat) -> CGFloat {
|
||||
return a + (b - a) * t
|
||||
}
|
||||
|
||||
func lerp(_ a: CGPoint, _ b: CGPoint, _ t: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t))
|
||||
}
|
||||
|
||||
func normalizeAngle(_ angle: CGFloat) -> CGFloat {
|
||||
var result = angle
|
||||
let twoPi = CGFloat.pi * 2
|
||||
while result > CGFloat.pi {
|
||||
result -= twoPi
|
||||
}
|
||||
while result <= -CGFloat.pi {
|
||||
result += twoPi
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeAngle(_ angle: Float) -> Float {
|
||||
var result = angle
|
||||
let twoPi = Float.pi * 2
|
||||
while result > Float.pi {
|
||||
result -= twoPi
|
||||
}
|
||||
while result <= -Float.pi {
|
||||
result += twoPi
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeAnglePositive(_ angle: Float) -> Float {
|
||||
var result = angle
|
||||
let twoPi = Float.pi * 2
|
||||
while result < 0 { result += twoPi }
|
||||
while result >= twoPi { result -= twoPi }
|
||||
return result
|
||||
}
|
||||
|
||||
func shortestAngleDelta(from: Float, to: Float) -> Float {
|
||||
return normalizeAngle(to - from)
|
||||
}
|
||||
|
||||
func nonZeroSign(_ value: Float, fallback: Float) -> Float {
|
||||
if value > 0 { return 1 }
|
||||
if value < 0 { return -1 }
|
||||
return fallback
|
||||
}
|
||||
|
||||
func snappedRightAngle(_ angle: CGFloat) -> CGFloat {
|
||||
let quarter = CGFloat.pi / 2
|
||||
let normalized = normalizeAngle(angle)
|
||||
let step = round(normalized / quarter)
|
||||
return step * quarter
|
||||
}
|
||||
|
||||
func rectToQuad(rect: CGRect, quad: Quad) -> CATransform3D {
|
||||
let x1a = quad.topLeft.x
|
||||
let y1a = quad.topLeft.y
|
||||
let x2a = quad.topRight.x
|
||||
let y2a = quad.topRight.y
|
||||
let x3a = quad.bottomLeft.x
|
||||
let y3a = quad.bottomLeft.y
|
||||
let x4a = quad.bottomRight.x
|
||||
let y4a = quad.bottomRight.y
|
||||
|
||||
let X = rect.origin.x
|
||||
let Y = rect.origin.y
|
||||
let W = rect.size.width
|
||||
let H = rect.size.height
|
||||
|
||||
let y21 = y2a - y1a
|
||||
let y32 = y3a - y2a
|
||||
let y43 = y4a - y3a
|
||||
let y14 = y1a - y4a
|
||||
let y31 = y3a - y1a
|
||||
let y42 = y4a - y2a
|
||||
|
||||
let a = -H * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42)
|
||||
let b = W * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43)
|
||||
let c = H * X * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42)
|
||||
- H * W * x1a * (x4a * y32 - x3a * y42 + x2a * y43)
|
||||
- W * Y * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43)
|
||||
|
||||
let d = H * (-x4a * y21 * y3a + x2a * y1a * y43 - x1a * y2a * y43 - x3a * y1a * y4a + x3a * y2a * y4a)
|
||||
let e = W * (x4a * y2a * y31 - x3a * y1a * y42 - x2a * y31 * y4a + x1a * y3a * y42)
|
||||
let f = -(
|
||||
W * (x4a * (Y * y2a * y31 + H * y1a * y32)
|
||||
- x3a * (H + Y) * y1a * y42
|
||||
+ H * x2a * y1a * y43
|
||||
+ x2a * Y * (y1a - y3a) * y4a
|
||||
+ x1a * Y * y3a * (-y2a + y4a))
|
||||
- H * X * (x4a * y21 * y3a - x2a * y1a * y43 + x3a * (y1a - y2a) * y4a + x1a * y2a * (-y3a + y4a))
|
||||
)
|
||||
|
||||
let g = H * (x3a * y21 - x4a * y21 + (-x1a + x2a) * y43)
|
||||
let h = W * (-x2a * y31 + x4a * y31 + (x1a - x3a) * y42)
|
||||
var i = W * Y * (x2a * y31 - x4a * y31 - x1a * y42 + x3a * y42)
|
||||
+ H * (X * (-(x3a * y21) + x4a * y21 + x1a * y43 - x2a * y43)
|
||||
+ W * (-(x3a * y2a) + x4a * y2a + x2a * y3a - x4a * y3a - x2a * y4a + x3a * y4a))
|
||||
|
||||
let epsilon: CGFloat = 0.0001
|
||||
if abs(i) < epsilon {
|
||||
i = i >= 0 ? epsilon : -epsilon
|
||||
}
|
||||
|
||||
return CATransform3D(
|
||||
m11: a / i, m12: d / i, m13: 0, m14: g / i,
|
||||
m21: b / i, m22: e / i, m23: 0, m24: h / i,
|
||||
m31: 0, m32: 0, m33: 1, m34: 0,
|
||||
m41: c / i, m42: f / i, m43: 0, m44: 1
|
||||
)
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
import AnimatedTextComponent
|
||||
|
||||
final class DialIndicatorComponent: Component {
|
||||
let content: AnyComponentWithIdentity<Empty>
|
||||
let backgroundColor: UIColor
|
||||
let foregroundColor: UIColor
|
||||
let diameter: CGFloat
|
||||
let contentSize: CGSize?
|
||||
let lineWidth: CGFloat
|
||||
let fontSize: CGFloat
|
||||
let progress: CGFloat
|
||||
let value: Int
|
||||
let suffix: String
|
||||
let isVisible: Bool
|
||||
let isFlipped: Bool
|
||||
|
||||
public init(
|
||||
content: AnyComponentWithIdentity<Empty>,
|
||||
backgroundColor: UIColor,
|
||||
foregroundColor: UIColor,
|
||||
diameter: CGFloat,
|
||||
contentSize: CGSize? = nil,
|
||||
lineWidth: CGFloat,
|
||||
fontSize: CGFloat,
|
||||
progress: CGFloat,
|
||||
value: Int,
|
||||
suffix: String,
|
||||
isVisible: Bool = true,
|
||||
isFlipped: Bool = false
|
||||
) {
|
||||
self.content = content
|
||||
self.backgroundColor = backgroundColor
|
||||
self.foregroundColor = foregroundColor
|
||||
self.diameter = diameter
|
||||
self.contentSize = contentSize
|
||||
self.lineWidth = lineWidth
|
||||
self.fontSize = fontSize
|
||||
self.progress = progress
|
||||
self.value = value
|
||||
self.suffix = suffix
|
||||
self.isVisible = isVisible
|
||||
self.isFlipped = isFlipped
|
||||
}
|
||||
|
||||
public static func ==(lhs: DialIndicatorComponent, rhs: DialIndicatorComponent) -> Bool {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.foregroundColor != rhs.foregroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.diameter != rhs.diameter {
|
||||
return false
|
||||
}
|
||||
if lhs.contentSize != rhs.contentSize {
|
||||
return false
|
||||
}
|
||||
if lhs.lineWidth != rhs.lineWidth {
|
||||
return false
|
||||
}
|
||||
if lhs.fontSize != rhs.fontSize {
|
||||
return false
|
||||
}
|
||||
if lhs.progress != rhs.progress {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
if lhs.suffix != rhs.suffix {
|
||||
return false
|
||||
}
|
||||
if lhs.isVisible != rhs.isVisible {
|
||||
return false
|
||||
}
|
||||
if lhs.isFlipped != rhs.isFlipped {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let containerView = UIView()
|
||||
private let backgroundLayer = SimpleShapeLayer()
|
||||
private let foregroundLayer = SimpleShapeLayer()
|
||||
|
||||
private var content = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
|
||||
private var component: DialIndicatorComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundLayer.lineCap = .round
|
||||
self.foregroundLayer.lineCap = .round
|
||||
|
||||
self.addSubview(self.containerView)
|
||||
|
||||
self.containerView.layer.addSublayer(self.backgroundLayer)
|
||||
self.containerView.layer.addSublayer(self.foregroundLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: DialIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let pathSize = CGSize(width: component.diameter, height: component.diameter)
|
||||
let pathFrame = CGRect(origin: .zero, size: pathSize).insetBy(dx: component.lineWidth * 0.5, dy: component.lineWidth * 0.5)
|
||||
|
||||
let strokeStart: CGFloat = 0.125
|
||||
let strokeEnd: CGFloat = 1.0 - strokeStart
|
||||
|
||||
self.backgroundLayer.lineWidth = component.lineWidth
|
||||
self.backgroundLayer.strokeColor = component.backgroundColor.cgColor
|
||||
self.backgroundLayer.fillColor = UIColor.clear.cgColor
|
||||
self.backgroundLayer.path = CGPath(ellipseIn: pathFrame, transform: nil)
|
||||
self.backgroundLayer.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
self.backgroundLayer.strokeStart = strokeStart
|
||||
self.backgroundLayer.strokeEnd = strokeEnd
|
||||
self.backgroundLayer.frame = CGRect(origin: .zero, size: pathSize)
|
||||
|
||||
self.foregroundLayer.lineWidth = component.lineWidth
|
||||
self.foregroundLayer.strokeColor = component.foregroundColor.cgColor
|
||||
self.foregroundLayer.fillColor = UIColor.clear.cgColor
|
||||
self.foregroundLayer.path = CGPath(ellipseIn: pathFrame, transform: nil)
|
||||
self.foregroundLayer.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
self.foregroundLayer.strokeStart = strokeStart
|
||||
transition.setShapeLayerStrokeEnd(layer: self.foregroundLayer, strokeEnd: strokeStart + (strokeEnd - strokeStart) * component.progress)
|
||||
self.foregroundLayer.frame = CGRect(origin: .zero, size: pathSize)
|
||||
|
||||
if previousComponent?.content.id != component.content.id {
|
||||
if let contentView = self.content.view {
|
||||
if transition.animation.isImmediate {
|
||||
contentView.removeFromSuperview()
|
||||
} else {
|
||||
transition.setScale(view: contentView, scale: 0.01)
|
||||
transition.setAlpha(view: contentView, alpha: 0.0, completion: { _ in
|
||||
contentView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
self.content = ComponentView()
|
||||
}
|
||||
|
||||
let contentSize = component.contentSize ?? CGSize(width: component.diameter - 16.0, height: component.diameter - 16.0)
|
||||
let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((pathSize.width - contentSize.width) / 2.0), y: floorToScreenPixels((pathSize.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
let _ = self.content.update(
|
||||
transition: .immediate,
|
||||
component: component.content.component,
|
||||
environment: {},
|
||||
containerSize: contentFrame.size
|
||||
)
|
||||
if let contentView = self.content.view {
|
||||
if contentView.superview == nil {
|
||||
self.containerView.addSubview(contentView)
|
||||
if !transition.animation.isImmediate {
|
||||
transition.animateScale(view: contentView, from: 0.01, to: 1.0)
|
||||
transition.animateAlpha(view: contentView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
contentView.frame = contentFrame
|
||||
}
|
||||
|
||||
var labelItems: [AnimatedTextComponent.Item] = [
|
||||
AnimatedTextComponent.Item(id: "percent", content: .number(component.value, minDigits: 1))
|
||||
]
|
||||
if !component.suffix.isEmpty {
|
||||
labelItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(component.suffix)))
|
||||
}
|
||||
|
||||
let labelSize = self.label.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
AnimatedTextComponent(
|
||||
font: Font.semibold(component.fontSize),
|
||||
color: component.foregroundColor,
|
||||
items: labelItems
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let labelView = self.label.view {
|
||||
if labelView.superview == nil {
|
||||
self.containerView.addSubview(labelView)
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((pathSize.width - labelSize.width) / 2.0) + 1.0 - UIScreenPixel, y: pathSize.height - labelSize.height + 2.0 - UIScreenPixel), size: labelSize))
|
||||
}
|
||||
|
||||
transition.setAlpha(view: self.containerView, alpha: component.isVisible ? 1.0 : 0.0)
|
||||
transition.setBlur(layer: self.containerView.layer, radius: component.isVisible ? 0.0 : 10.0)
|
||||
|
||||
self.containerView.transform = CGAffineTransform(rotationAngle: component.isFlipped ? .pi : 0.0)
|
||||
|
||||
self.containerView.frame = CGRect(origin: .zero, size: pathSize)
|
||||
|
||||
return pathSize
|
||||
}
|
||||
}
|
||||
|
||||
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 ColorSwatchComponent: Component {
|
||||
let innerColor: UIColor
|
||||
let outerColor: UIColor
|
||||
|
||||
public init(
|
||||
innerColor: UIColor,
|
||||
outerColor: UIColor
|
||||
) {
|
||||
self.innerColor = innerColor
|
||||
self.outerColor = outerColor
|
||||
}
|
||||
|
||||
public static func ==(lhs: ColorSwatchComponent, rhs: ColorSwatchComponent) -> Bool {
|
||||
if lhs.innerColor != rhs.innerColor {
|
||||
return false
|
||||
}
|
||||
if lhs.outerColor != rhs.outerColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIImageView {
|
||||
private var component: ColorSwatchComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ColorSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if previousComponent?.innerColor != component.innerColor || previousComponent?.outerColor != component.outerColor {
|
||||
self.image = generateImage(availableSize, contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
if let image = UIImage(bundleImageName: "Premium/Craft/DialColorMask"), let cgImage = image.cgImage {
|
||||
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
|
||||
}
|
||||
var locations: [CGFloat] = [1.0, 0.95, 0.1, 0.0]
|
||||
let colors: [CGColor] = [component.innerColor.cgColor, component.innerColor.cgColor, component.outerColor.cgColor, component.outerColor.cgColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ViewControllerComponent
|
||||
import SheetComponent
|
||||
import BundleIconComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import ButtonComponent
|
||||
import GiftItemComponent
|
||||
import AccountContext
|
||||
import GlassBarButtonComponent
|
||||
|
||||
private func giftCraftRibbonColor(for gift: StarGift.UniqueGift) -> GiftItemComponent.Ribbon.Color {
|
||||
for attribute in gift.attributes {
|
||||
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
return .custom(outerColor, innerColor)
|
||||
}
|
||||
}
|
||||
return .blue
|
||||
}
|
||||
|
||||
private final class GiftCraftSheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let gift: StarGift.UniqueGift
|
||||
let dismiss: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
dismiss: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.gift = gift
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftCraftSheetContent, rhs: GiftCraftSheetContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let closeButton = Child(GlassBarButtonComponent.self)
|
||||
let title = Child(BalancedTextComponent.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let gift = Child(GiftItemComponent.self)
|
||||
let button = Child(ButtonComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let component = context.component
|
||||
let theme = environment.theme
|
||||
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
|
||||
|
||||
let closeButton = closeButton.update(
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 40.0, height: 40.0),
|
||||
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
|
||||
isDark: theme.overallDarkAppearance,
|
||||
state: .generic,
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "close",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { _ in
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 40.0, height: 40.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(closeButton.position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 36.0)))
|
||||
|
||||
let title = title.update(
|
||||
component: BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Gift Crafting", font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 1,
|
||||
lineSpacing: 0.1
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 96.0, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(title.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)))
|
||||
contentSize.height += title.size.height + 16.0
|
||||
|
||||
let giftSize = CGSize(width: 140.0, height: 140.0)
|
||||
let gift = gift.update(
|
||||
component: GiftItemComponent(
|
||||
context: component.context,
|
||||
style: .glass,
|
||||
theme: theme,
|
||||
strings: environment.strings,
|
||||
subject: .uniqueGift(gift: component.gift, price: nil),
|
||||
ribbon: GiftItemComponent.Ribbon(
|
||||
text: "#\(component.gift.number)",
|
||||
font: .monospaced,
|
||||
color: giftCraftRibbonColor(for: component.gift)
|
||||
),
|
||||
mode: .grid
|
||||
),
|
||||
availableSize: giftSize,
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(gift.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + gift.size.height / 2.0)))
|
||||
contentSize.height += gift.size.height + 16.0
|
||||
|
||||
let text = text.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: "This Swiftgram gift crafting flow is temporarily disabled in the merged build while the underlying APIs are being adapted.",
|
||||
font: Font.regular(15.0),
|
||||
textColor: theme.actionSheet.secondaryTextColor,
|
||||
paragraphAlignment: .center
|
||||
)
|
||||
),
|
||||
maximumNumberOfLines: 0
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 48.0, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(text.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)))
|
||||
contentSize.height += text.size.height + 24.0
|
||||
|
||||
let button = button.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "ok",
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: environment.strings.Common_OK,
|
||||
font: Font.semibold(17.0),
|
||||
textColor: theme.list.itemCheckColors.foregroundColor,
|
||||
paragraphAlignment: .center
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 60.0, height: 52.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(button.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)).cornerRadius(10.0))
|
||||
contentSize.height += button.size.height + 16.0 + environment.safeInsets.bottom
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class GiftCraftScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let gift: StarGift.UniqueGift
|
||||
|
||||
init(context: AccountContext, gift: StarGift.UniqueGift) {
|
||||
self.context = context
|
||||
self.gift = gift
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftCraftScreenComponent, rhs: GiftCraftScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>()
|
||||
private let sheetAnimateOut = ActionSlot<Action<Void>>()
|
||||
|
||||
private var component: GiftCraftScreenComponent?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(
|
||||
component: GiftCraftScreenComponent,
|
||||
availableSize: CGSize,
|
||||
state: EmptyComponentState,
|
||||
environment: Environment<ViewControllerComponentContainer.Environment>,
|
||||
transition: ComponentTransition
|
||||
) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
self.environment = environment
|
||||
|
||||
let sheetEnvironment = SheetComponentEnvironment(
|
||||
isDisplaying: environment.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||
dismiss: { [weak self] _ in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
self.sheetAnimateOut.invoke(Action { _ in
|
||||
environment.controller()?.dismiss(completion: nil)
|
||||
})
|
||||
}
|
||||
)
|
||||
let _ = self.sheet.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
SheetComponent(
|
||||
content: AnyComponent(
|
||||
GiftCraftSheetContent(
|
||||
context: component.context,
|
||||
gift: component.gift,
|
||||
dismiss: { [weak self] in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
self.sheetAnimateOut.invoke(Action { _ in
|
||||
environment.controller()?.dismiss(completion: nil)
|
||||
})
|
||||
}
|
||||
)
|
||||
),
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
animateOut: self.sheetAnimateOut
|
||||
)
|
||||
),
|
||||
environment: {
|
||||
environment
|
||||
sheetEnvironment
|
||||
},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let sheetView = self.sheet.view {
|
||||
if sheetView.superview == nil {
|
||||
self.addSubview(sheetView)
|
||||
}
|
||||
transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(
|
||||
view: View,
|
||||
availableSize: CGSize,
|
||||
state: EmptyComponentState,
|
||||
environment: Environment<ViewControllerComponentContainer.Environment>,
|
||||
transition: ComponentTransition
|
||||
) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class GiftCraftScreen: ViewControllerComponentContainer {
|
||||
fileprivate weak var profileGiftsContext: ProfileGiftsContext?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
profileGiftsContext: ProfileGiftsContext?
|
||||
) {
|
||||
self.profileGiftsContext = profileGiftsContext
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: GiftCraftScreenComponent(context: context, gift: gift),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
presentationMode: .modal,
|
||||
theme: .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
} else {
|
||||
self.dismiss(completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
+714
@@ -0,0 +1,714 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramStringFormatting
|
||||
import ViewControllerComponent
|
||||
import BundleIconComponent
|
||||
import MultilineTextComponent
|
||||
import GiftItemComponent
|
||||
import AccountContext
|
||||
import AnimatedTextComponent
|
||||
import Markdown
|
||||
import PresentationDataUtils
|
||||
import GiftViewScreen
|
||||
import NavigationStackComponent
|
||||
import GiftStoreScreen
|
||||
import ResizableSheetComponent
|
||||
import TooltipUI
|
||||
import GlassBarButtonComponent
|
||||
import ConfettiEffect
|
||||
import GiftLoadingShimmerView
|
||||
|
||||
final class SelectGiftPageContent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let craftContext: CraftGiftsContext
|
||||
let resaleContext: ResaleGiftsContext
|
||||
let gift: StarGift.UniqueGift
|
||||
let genericGift: StarGift.Gift
|
||||
let selectedGiftIds: Set<Int64>
|
||||
let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>
|
||||
let selectGift: (GiftItem) -> Void
|
||||
let dismiss: () -> Void
|
||||
let boundsUpdated: ActionSlot<ResizableSheetComponentEnvironment.BoundsUpdate>
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
craftContext: CraftGiftsContext,
|
||||
resaleContext: ResaleGiftsContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
genericGift: StarGift.Gift,
|
||||
selectedGiftIds: Set<Int64>,
|
||||
starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>,
|
||||
selectGift: @escaping (GiftItem) -> Void,
|
||||
dismiss: @escaping () -> Void,
|
||||
boundsUpdated: ActionSlot<ResizableSheetComponentEnvironment.BoundsUpdate>
|
||||
) {
|
||||
self.context = context
|
||||
self.craftContext = craftContext
|
||||
self.resaleContext = resaleContext
|
||||
self.gift = gift
|
||||
self.genericGift = genericGift
|
||||
self.selectedGiftIds = selectedGiftIds
|
||||
self.starsTopUpOptions = starsTopUpOptions
|
||||
self.selectGift = selectGift
|
||||
self.dismiss = dismiss
|
||||
self.boundsUpdated = boundsUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: SelectGiftPageContent, rhs: SelectGiftPageContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedGiftIds != rhs.selectedGiftIds {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let myGiftsTitle = ComponentView<Empty>()
|
||||
private var gifts: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private let myGiftsPlaceholder = ComponentView<Empty>()
|
||||
private let loadingView = GiftLoadingShimmerView()
|
||||
|
||||
private let storeGiftsTitle = ComponentView<Empty>()
|
||||
private let storeGifts = ComponentView<Empty>()
|
||||
|
||||
private var craftState: CraftGiftsContext.State?
|
||||
private var craftStateDisposable: Disposable?
|
||||
|
||||
private var availableGifts: [GiftItem] = []
|
||||
private var giftMap: [Int64: ProfileGiftsContext.State.StarGift] = [:]
|
||||
|
||||
private var availableSize: CGSize?
|
||||
private var currentBounds: CGRect?
|
||||
|
||||
private var component: SelectGiftPageContent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.cornerRadius = 40.0
|
||||
self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
|
||||
self.addSubview(self.loadingView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.craftStateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func updateScrolling(interactive: Bool, transition: ComponentTransition) -> CGFloat {
|
||||
guard let bounds = self.currentBounds, let availableSize = self.availableSize, let component = self.component, let environment = self.environment else {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
let visibleBounds = bounds.insetBy(dx: 0.0, dy: -10.0)
|
||||
|
||||
var contentHeight: CGFloat = 88.0 + 32.0
|
||||
|
||||
let itemSpacing: CGFloat = 10.0
|
||||
let itemSideInset = 16.0
|
||||
let itemsInRow: Int
|
||||
if availableSize.width > availableSize.height || availableSize.width > 480.0 {
|
||||
if case .tablet = environment.deviceMetrics.type {
|
||||
itemsInRow = 4
|
||||
} else {
|
||||
itemsInRow = 5
|
||||
}
|
||||
} else {
|
||||
itemsInRow = 3
|
||||
}
|
||||
let itemWidth = (availableSize.width - itemSideInset * 2.0 - itemSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow)
|
||||
let itemSize = CGSize(width: itemWidth, height: itemWidth)
|
||||
|
||||
var isLoading = false
|
||||
if self.availableGifts.isEmpty, case .loading = (self.craftState?.dataState ?? .loading) {
|
||||
isLoading = true
|
||||
}
|
||||
let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25)
|
||||
let loadingSize = CGSize(width: availableSize.width, height: 180.0)
|
||||
if isLoading {
|
||||
contentHeight += 120.0
|
||||
self.loadingView.update(size: loadingSize, theme: environment.theme, itemSize: itemSize, showFilters: false, isPlain: true, transition: .immediate)
|
||||
loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0)
|
||||
} else {
|
||||
loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0)
|
||||
}
|
||||
transition.setFrame(view: self.loadingView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 170.0), size: loadingSize))
|
||||
|
||||
var itemFrame = CGRect(origin: CGPoint(x: itemSideInset, y: contentHeight), size: itemSize)
|
||||
var itemsHeight: CGFloat = 0.0
|
||||
var validIds: [AnyHashable] = []
|
||||
for gift in self.availableGifts {
|
||||
var isVisible = false
|
||||
if visibleBounds.intersects(itemFrame) {
|
||||
isVisible = true
|
||||
}
|
||||
if isVisible {
|
||||
let itemId = AnyHashable(gift.gift.id)
|
||||
validIds.append(itemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.gifts[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
self.gifts[itemId] = visibleItem
|
||||
itemTransition = .immediate
|
||||
}
|
||||
|
||||
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
|
||||
let ribbonText = "#\(gift.gift.number)"
|
||||
for attribute in gift.gift.attributes {
|
||||
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
ribbonColor = .custom(outerColor, innerColor)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: component.context,
|
||||
style: .glass,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
peer: nil,
|
||||
subject: .uniqueGift(gift: gift.gift, price: nil),
|
||||
ribbon: GiftItemComponent.Ribbon(text: ribbonText, font: .monospaced, color: ribbonColor, outline: nil),
|
||||
badge: gift.gift.craftChancePermille.flatMap { "+\($0 / 10)%" },
|
||||
resellPrice: nil,
|
||||
isHidden: false,
|
||||
isSelected: false,
|
||||
isPinned: false,
|
||||
isEditing: false,
|
||||
mode: .grid,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
HapticFeedback().impact(.light)
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if let profileGift = self.giftMap[gift.gift.id], let canCraftDate = profileGift.canCraftAt, currentTime < canCraftDate {
|
||||
let dateString = stringForFullDate(timestamp: canCraftDate, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)
|
||||
let alertController = textAlertController(
|
||||
context: component.context,
|
||||
title: environment.strings.Gift_Craft_Unavailable_Title,
|
||||
text: environment.strings.Gift_Craft_Unavailable_Text(dateString).string,
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})
|
||||
],
|
||||
parseMarkdown: true
|
||||
)
|
||||
environment.controller()?.present(alertController, in: .window(.root))
|
||||
return
|
||||
}
|
||||
|
||||
component.selectGift(gift)
|
||||
component.dismiss()
|
||||
},
|
||||
contextAction: { _, _ in }
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: itemSize
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
if itemView.superview == nil {
|
||||
if let _ = self.loadingView.superview {
|
||||
self.insertSubview(itemView, belowSubview: self.loadingView)
|
||||
} else {
|
||||
self.addSubview(itemView)
|
||||
}
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
|
||||
itemsHeight = itemFrame.maxY - contentHeight
|
||||
|
||||
itemFrame.origin.x += itemFrame.width + itemSpacing
|
||||
if itemFrame.maxX > availableSize.width {
|
||||
itemFrame.origin.x = itemSideInset
|
||||
itemFrame.origin.y += itemSize.height + itemSpacing
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, item) in self.gifts {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = item.view {
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
|
||||
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
itemView.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.gifts.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
if let state = self.craftState, case .ready = state.dataState, self.availableGifts.isEmpty {
|
||||
contentHeight += 10.0
|
||||
let myGiftsPlaceholderSize = self.myGiftsPlaceholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_NoGiftsFromCollection, font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 3,
|
||||
lineSpacing: 0.1
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 32.0, height: .greatestFiniteMagnitude)
|
||||
)
|
||||
let myGiftsPlaceholderFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - myGiftsPlaceholderSize.width) / 2.0), y: contentHeight), size: myGiftsPlaceholderSize)
|
||||
if let myGiftsPlaceholderView = self.myGiftsPlaceholder.view {
|
||||
if myGiftsPlaceholderView.superview == nil {
|
||||
self.addSubview(myGiftsPlaceholderView)
|
||||
}
|
||||
myGiftsPlaceholderView.frame = myGiftsPlaceholderFrame
|
||||
}
|
||||
contentHeight += myGiftsPlaceholderSize.height
|
||||
contentHeight += 32.0
|
||||
} else {
|
||||
contentHeight += itemsHeight
|
||||
contentHeight += 24.0
|
||||
}
|
||||
|
||||
if let storeGiftsView = self.storeGifts.view as? GiftStoreContentComponent.View {
|
||||
storeGiftsView.updateScrolling(bounds: bounds.offsetBy(dx: 0.0, dy: -contentHeight), interactive: interactive, transition: .immediate)
|
||||
}
|
||||
|
||||
let bottomContentOffset = max(0.0, contentHeight - bounds.origin.y - bounds.height)
|
||||
if interactive, bottomContentOffset < 800.0 {
|
||||
Queue.mainQueue().justDispatch {
|
||||
component.craftContext.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
return contentHeight
|
||||
}
|
||||
|
||||
func update(component: SelectGiftPageContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.availableSize = availableSize
|
||||
if self.component == nil {
|
||||
self.currentBounds = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
component.boundsUpdated.connect { [weak self] update in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentBounds = update.bounds
|
||||
let _ = self.updateScrolling(interactive: update.isInteractive, transition: .immediate)
|
||||
}
|
||||
|
||||
let initialGiftItem = GiftItem(
|
||||
gift: component.gift,
|
||||
reference: .slug(slug: component.gift.slug)
|
||||
)
|
||||
self.availableGifts = [
|
||||
initialGiftItem
|
||||
]
|
||||
|
||||
self.craftStateDisposable = (component.craftContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.craftState = state
|
||||
|
||||
var items: [GiftItem] = []
|
||||
var giftMap: [Int64: ProfileGiftsContext.State.StarGift] = [:]
|
||||
var existingIds = Set<Int64>()
|
||||
for gift in state.gifts {
|
||||
guard let reference = gift.reference, case let .unique(uniqueGift) = gift.gift, !existingIds.contains(uniqueGift.id) else {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(uniqueGift.id)
|
||||
|
||||
let giftItem = GiftItem(
|
||||
gift: uniqueGift,
|
||||
reference: reference
|
||||
)
|
||||
giftMap[uniqueGift.id] = gift
|
||||
|
||||
if component.selectedGiftIds.contains(uniqueGift.id) {
|
||||
continue
|
||||
}
|
||||
items.append(giftItem)
|
||||
}
|
||||
|
||||
self.availableGifts = items
|
||||
self.giftMap = giftMap
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
self.environment = environment
|
||||
|
||||
self.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor
|
||||
|
||||
var contentHeight: CGFloat = 88.0
|
||||
|
||||
let myGiftsTitleSize = self.myGiftsTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_YourGifts.uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||
)
|
||||
let myGiftsTitleFrame = CGRect(origin: CGPoint(x: 26.0, y: contentHeight), size: myGiftsTitleSize)
|
||||
if let myGiftsTitleView = self.myGiftsTitle.view {
|
||||
if myGiftsTitleView.superview == nil {
|
||||
self.addSubview(myGiftsTitleView)
|
||||
}
|
||||
transition.setFrame(view: myGiftsTitleView, frame: myGiftsTitleFrame)
|
||||
}
|
||||
|
||||
contentHeight += 32.0
|
||||
|
||||
contentHeight = self.updateScrolling(interactive: false, transition: transition)
|
||||
|
||||
let resaleCount = component.genericGift.availability?.resale ?? 0
|
||||
let saleTitle = environment.strings.Gift_Craft_Select_SaleGiftsCount(Int32(clamping: resaleCount)).uppercased()
|
||||
|
||||
let storeGiftsTitleSize = self.storeGiftsTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: saleTitle, font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||
)
|
||||
let storeGiftsTitleFrame = CGRect(origin: CGPoint(x: 26.0, y: contentHeight), size: storeGiftsTitleSize)
|
||||
if let storeGiftsTitleView = self.storeGiftsTitle.view {
|
||||
if storeGiftsTitleView.superview == nil {
|
||||
self.addSubview(storeGiftsTitleView)
|
||||
}
|
||||
transition.setFrame(view: storeGiftsTitleView, frame: storeGiftsTitleFrame)
|
||||
}
|
||||
contentHeight += 28.0
|
||||
|
||||
self.storeGifts.parentState = state
|
||||
let storeGiftsSize = self.storeGifts.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
GiftStoreContentComponent(
|
||||
context: component.context,
|
||||
resaleGiftsContext: component.resaleContext,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
safeInsets: UIEdgeInsets(),
|
||||
statusBarHeight: contentHeight - 62.0,
|
||||
navigationHeight: 0.0,
|
||||
overNavigationContainer: self,
|
||||
starsContext: component.context.starsContext!,
|
||||
peerId: component.context.account.peerId,
|
||||
gift: component.genericGift,
|
||||
isPlain: true,
|
||||
confirmPurchaseImmediately: true,
|
||||
starsTopUpOptions: component.starsTopUpOptions,
|
||||
scrollToTop: {},
|
||||
controller: environment.controller,
|
||||
completion: { [weak self] uniqueGift in
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() as? SelectCraftGiftScreen, let navigationController = controller.navigationController else {
|
||||
return
|
||||
}
|
||||
let giftItem = GiftItem(gift: uniqueGift, reference: .slug(slug: uniqueGift.slug))
|
||||
component.selectGift(giftItem)
|
||||
component.dismiss()
|
||||
|
||||
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
|
||||
|
||||
Queue.mainQueue().after(1.0) {
|
||||
component.craftContext.reload()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude)
|
||||
)
|
||||
let storeGiftsFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: storeGiftsSize)
|
||||
if let storeGiftsView = self.storeGifts.view as? GiftStoreContentComponent.View {
|
||||
if storeGiftsView.superview == nil {
|
||||
self.insertSubview(storeGiftsView, at: 0)
|
||||
}
|
||||
transition.setFrame(view: storeGiftsView, frame: storeGiftsFrame)
|
||||
|
||||
storeGiftsView.updateScrolling(bounds: CGRect(origin: .zero, size: availableSize), transition: .immediate)
|
||||
}
|
||||
contentHeight += storeGiftsSize.height
|
||||
contentHeight += 90.0
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class SheetContainerComponent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let craftContext: CraftGiftsContext
|
||||
let resaleContext: ResaleGiftsContext
|
||||
let gift: StarGift.UniqueGift
|
||||
let genericGift: StarGift.Gift
|
||||
let selectedGiftIds: Set<Int64>
|
||||
let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>
|
||||
let selectGift: (GiftItem) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
craftContext: CraftGiftsContext,
|
||||
resaleContext: ResaleGiftsContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
genericGift: StarGift.Gift,
|
||||
selectedGiftIds: Set<Int64>,
|
||||
starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>,
|
||||
selectGift: @escaping (GiftItem) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.craftContext = craftContext
|
||||
self.resaleContext = resaleContext
|
||||
self.gift = gift
|
||||
self.genericGift = genericGift
|
||||
self.selectedGiftIds = selectedGiftIds
|
||||
self.starsTopUpOptions = starsTopUpOptions
|
||||
self.selectGift = selectGift
|
||||
}
|
||||
|
||||
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State()
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let sheet = Child(ResizableSheetComponent<EnvironmentType>.self)
|
||||
let animateOut = StoredActionSlot(Action<Void>.self)
|
||||
|
||||
let boundsUpdated = ActionSlot<ResizableSheetComponentEnvironment.BoundsUpdate>()
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
let dismiss: (Bool) -> Void = { animated in
|
||||
if animated {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let theme = environment.theme
|
||||
|
||||
let backgroundColor = environment.theme.list.modalPlainBackgroundColor
|
||||
|
||||
let sheet = sheet.update(
|
||||
component: ResizableSheetComponent<EnvironmentType>(
|
||||
content: AnyComponent<EnvironmentType>(
|
||||
SelectGiftPageContent(
|
||||
context: component.context,
|
||||
craftContext: component.craftContext,
|
||||
resaleContext: component.resaleContext,
|
||||
gift: component.gift,
|
||||
genericGift: component.genericGift,
|
||||
selectedGiftIds: component.selectedGiftIds,
|
||||
starsTopUpOptions: component.starsTopUpOptions,
|
||||
selectGift: component.selectGift,
|
||||
dismiss: {
|
||||
dismiss(true)
|
||||
},
|
||||
boundsUpdated: boundsUpdated
|
||||
)
|
||||
),
|
||||
titleItem: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_Title, font: Font.semibold(17.0), textColor: environment.theme.actionSheet.primaryTextColor)))
|
||||
),
|
||||
leftItem: AnyComponent(
|
||||
GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: theme.overallDarkAppearance,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { _ in
|
||||
dismiss(true)
|
||||
}
|
||||
)
|
||||
),
|
||||
rightItem: nil,
|
||||
bottomItem: nil,
|
||||
backgroundColor: .color(backgroundColor),
|
||||
isFullscreen: false,
|
||||
animateOut: animateOut
|
||||
),
|
||||
environment: {
|
||||
environment
|
||||
ResizableSheetComponentEnvironment(
|
||||
theme: theme,
|
||||
statusBarHeight: environment.statusBarHeight,
|
||||
safeInsets: environment.safeInsets,
|
||||
metrics: environment.metrics,
|
||||
deviceMetrics: environment.deviceMetrics,
|
||||
isDisplaying: environment.value.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
screenSize: context.availableSize,
|
||||
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||
dismiss: { animated in
|
||||
dismiss(animated)
|
||||
},
|
||||
boundsUpdated: boundsUpdated
|
||||
)
|
||||
},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(sheet
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class SelectCraftGiftScreen: ViewControllerComponentContainer {
|
||||
public init(
|
||||
context: AccountContext,
|
||||
craftContext: CraftGiftsContext,
|
||||
resaleContext: ResaleGiftsContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
genericGift: StarGift.Gift,
|
||||
selectedGiftIds: Set<Int64>,
|
||||
starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>,
|
||||
selectGift: @escaping (GiftItem) -> Void
|
||||
) {
|
||||
super.init(
|
||||
context: context,
|
||||
component: SheetContainerComponent(
|
||||
context: context,
|
||||
craftContext: craftContext,
|
||||
resaleContext: resaleContext,
|
||||
gift: gift,
|
||||
genericGift: genericGift,
|
||||
selectedGiftIds: selectedGiftIds,
|
||||
starsTopUpOptions: starsTopUpOptions,
|
||||
selectGift: selectGift
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
theme: .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
fileprivate func dismissAllTooltips() {
|
||||
self.window?.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
controller.dismiss(inPlace: false)
|
||||
}
|
||||
})
|
||||
self.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
controller.dismiss(inPlace: false)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
self.dismissAllTooltips()
|
||||
|
||||
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -515,7 +515,7 @@ public final class GiftItemComponent: Component {
|
||||
animationOffset = 16.0
|
||||
for attribute in gift.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
animationFile = file
|
||||
if !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
@@ -606,7 +606,7 @@ public final class GiftItemComponent: Component {
|
||||
}
|
||||
for attribute in attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
animationFile = file
|
||||
if !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
|
||||
+2
-1
@@ -755,6 +755,7 @@ private final class GiftSetupScreenComponent: Component {
|
||||
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .builtin(WallpaperSettings()),
|
||||
theme: presentationData.theme,
|
||||
preferredGlassType: .default,
|
||||
strings: presentationData.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
@@ -1604,7 +1605,7 @@ private final class GiftSetupScreenComponent: Component {
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller(), let upgradePreview else {
|
||||
return
|
||||
}
|
||||
let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, attributes: upgradePreview.attributes, peerName: peerName)
|
||||
let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, gift: gift, attributes: upgradePreview.attributes, peerName: peerName)
|
||||
controller.push(previewController)
|
||||
})
|
||||
}
|
||||
|
||||
+3
-3
@@ -50,7 +50,7 @@ private func actionForAttribute(attribute: StarGift.UniqueGift.Attribute, presen
|
||||
let searchComponents = searchQuery.lowercased().components(separatedBy: .whitespaces).filter { !$0.isEmpty }
|
||||
|
||||
switch attribute {
|
||||
case let .model(name, file, _), let .pattern(name, file, _):
|
||||
case let .model(name, file, _, _), let .pattern(name, file, _):
|
||||
let attributeId: ResaleGiftsContext.Attribute
|
||||
if case .model = attribute {
|
||||
attributeId = .model(file.fileId.id)
|
||||
@@ -320,7 +320,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
|
||||
|
||||
private func getAttributeId(from attribute: StarGift.UniqueGift.Attribute) -> AnyHashable {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
return AnyHashable("model_\(file.fileId.id)")
|
||||
case let .pattern(_, file, _):
|
||||
return AnyHashable("pattern_\(file.fileId.id)")
|
||||
@@ -582,7 +582,7 @@ private func filteredAttributes(attributes: [StarGift.UniqueGift.Attribute], que
|
||||
for attribute in attributes {
|
||||
let string: String
|
||||
switch attribute {
|
||||
case let .model(name, _, _):
|
||||
case let .model(name, _, _, _):
|
||||
string = name
|
||||
case let .pattern(name, _, _):
|
||||
string = name
|
||||
|
||||
+3
-3
@@ -537,8 +537,8 @@ final class GiftStoreScreenComponent: Component {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}.sorted(by: { lhs, rhs in
|
||||
if case let .model(_, lhsFile, _) = lhs, case let .model(_, rhsFile, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.model(lhsFile.fileId.id)], let rhsCount = self.state?.starGiftsState?.attributeCount[.model(rhsFile.fileId.id)] {
|
||||
}.sorted(by: { (lhs: StarGift.UniqueGift.Attribute, rhs: StarGift.UniqueGift.Attribute) in
|
||||
if case let .model(_, lhsFile, _, _) = lhs, case let .model(_, rhsFile, _, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.model(lhsFile.fileId.id)], let rhsCount = self.state?.starGiftsState?.attributeCount[.model(rhsFile.fileId.id)] {
|
||||
return lhsCount > rhsCount
|
||||
} else {
|
||||
return false
|
||||
@@ -1231,7 +1231,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.gift = gift
|
||||
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: gift.id)
|
||||
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: gift.id, forCrafting: false)
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
+4
-4
@@ -164,7 +164,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
|
||||
}).shuffled().prefix(5))
|
||||
self.previewSymbols = randomSymbols
|
||||
|
||||
for case let .model(_, file, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
for case let .model(_, file, _, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
self.fetchedFiles.insert(file.fileId.id)
|
||||
}
|
||||
@@ -725,9 +725,9 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
|
||||
if let genericGift {
|
||||
var attributes: [StarGift.UniqueGift.Attribute] = []
|
||||
if state.previewModelIndex == -1 {
|
||||
attributes.append(.model(name: "", file: genericGift.file, rarity: 0))
|
||||
attributes.append(.model(name: "", file: genericGift.file, rarity: .permille(0), crafted: false))
|
||||
if let background = genericGift.background {
|
||||
attributes.append(.backdrop(name: "", id: 0, innerColor: background.centerColor, outerColor: background.edgeColor, patternColor: 0, textColor: 0, rarity: 0))
|
||||
attributes.append(.backdrop(name: "", id: 0, innerColor: background.centerColor, outerColor: background.edgeColor, patternColor: 0, textColor: 0, rarity: .permille(0)))
|
||||
}
|
||||
} else if !state.previewModels.isEmpty {
|
||||
attributes.append(state.previewModels[state.previewModelIndex])
|
||||
@@ -1082,7 +1082,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
|
||||
guard let state, let attributes = state.giftUpgradeAttributes else {
|
||||
return
|
||||
}
|
||||
let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(context: component.context, gift: .generic(gift), attributes: attributes, selectedAttributes: nil, focusedAttribute: nil)
|
||||
let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(context: component.context, gift: .generic(gift), crafted: false, attributes: attributes, selectedAttributes: nil, focusedAttribute: nil)
|
||||
environment.controller()?.push(variantsController)
|
||||
}, animateScale: false),
|
||||
availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height),
|
||||
|
||||
+4
-4
@@ -83,23 +83,23 @@ public func giftOfferAlertController(
|
||||
let tag: AnyObject?
|
||||
|
||||
switch attribute {
|
||||
case let .model(name, _, rarity):
|
||||
case let .model(name, _, rarity, _):
|
||||
id = "model"
|
||||
title = strings.Gift_Unique_Model
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = modelButtonTag
|
||||
case let .backdrop(name, _, _, _, _, _, rarity):
|
||||
id = "backdrop"
|
||||
title = strings.Gift_Unique_Backdrop
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = backdropButtonTag
|
||||
case let .pattern(name, _, rarity):
|
||||
id = "pattern"
|
||||
title = strings.Gift_Unique_Symbol
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = symbolButtonTag
|
||||
case .originalInfo:
|
||||
continue
|
||||
|
||||
+4
-4
@@ -68,23 +68,23 @@ public func giftTransferAlertController(
|
||||
let tag: AnyObject?
|
||||
|
||||
switch attribute {
|
||||
case let .model(name, _, rarity):
|
||||
case let .model(name, _, rarity, _):
|
||||
id = "model"
|
||||
title = strings.Gift_Unique_Model
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = modelButtonTag
|
||||
case let .backdrop(name, _, _, _, _, _, rarity):
|
||||
id = "backdrop"
|
||||
title = strings.Gift_Unique_Backdrop
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = backdropButtonTag
|
||||
case let .pattern(name, _, rarity):
|
||||
id = "pattern"
|
||||
title = strings.Gift_Unique_Symbol
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = symbolButtonTag
|
||||
case .originalInfo:
|
||||
continue
|
||||
|
||||
+8
-8
@@ -419,11 +419,11 @@ private final class GiftUpgradePreviewScreenComponent: Component {
|
||||
var isSelected = false
|
||||
for attribute in attributeList {
|
||||
switch attribute {
|
||||
case let .model(name, file, rarityValue):
|
||||
case let .model(name, file, rarityValue, _):
|
||||
itemId += "\(file.fileId.id)"
|
||||
if self.selectedSection == .models {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
modelAttribute = attribute
|
||||
|
||||
isSelected = self.selectedModel == attribute
|
||||
@@ -432,7 +432,7 @@ private final class GiftUpgradePreviewScreenComponent: Component {
|
||||
itemId += "\(id)"
|
||||
if self.selectedSection == .backdrops {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
backdropAttribute = attribute
|
||||
|
||||
isSelected = self.selectedBackdrop == attribute
|
||||
@@ -441,7 +441,7 @@ private final class GiftUpgradePreviewScreenComponent: Component {
|
||||
itemId += "\(file.fileId.id)"
|
||||
if self.selectedSection == .symbols {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
symbolAttribute = attribute
|
||||
|
||||
isSelected = self.selectedSymbol == attribute
|
||||
@@ -1210,18 +1210,18 @@ private final class AttributeInfoComponent: Component {
|
||||
let subtitle: String
|
||||
let rarity: Int32
|
||||
switch component.attribute {
|
||||
case let .model(name, _, rarityValue):
|
||||
case let .model(name, _, rarityValue, _):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Model
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
case let .backdrop(name, _, _, _, _, _, rarityValue):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Backdrop
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
case let .pattern(name, _, rarityValue):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Symbol
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
default:
|
||||
title = ""
|
||||
subtitle = ""
|
||||
|
||||
+9
-9
@@ -425,14 +425,14 @@ private final class GiftUpgradeVariantsScreenComponent: Component {
|
||||
var isSelected = false
|
||||
for attribute in attributeList {
|
||||
switch attribute {
|
||||
case let .model(name, file, rarityValue):
|
||||
case let .model(name, file, rarityValue, _):
|
||||
itemId += "\(file.fileId.id)"
|
||||
if self.selectedSection == .models {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
modelAttribute = attribute
|
||||
|
||||
if case let .model(_, selectedFile, _) = self.selectedModel {
|
||||
if case let .model(_, selectedFile, _, _) = self.selectedModel {
|
||||
isSelected = file.fileId == selectedFile.fileId
|
||||
} else {
|
||||
isSelected = false
|
||||
@@ -442,7 +442,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component {
|
||||
itemId += "\(id)"
|
||||
if self.selectedSection == .backdrops {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
backdropAttribute = attribute
|
||||
|
||||
if case let .backdrop(_, selectedId, _, _, _, _, _) = self.selectedBackdrop {
|
||||
@@ -455,7 +455,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component {
|
||||
itemId += "\(file.fileId.id)"
|
||||
if self.selectedSection == .symbols {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
symbolAttribute = attribute
|
||||
|
||||
if case let .pattern(_, selectedFile, _) = self.selectedSymbol {
|
||||
@@ -1270,18 +1270,18 @@ private final class AttributeInfoComponent: Component {
|
||||
let subtitle: String
|
||||
let rarity: Int32
|
||||
switch component.attribute {
|
||||
case let .model(name, _, rarityValue):
|
||||
case let .model(name, _, rarityValue, _):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Model
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
case let .backdrop(name, _, _, _, _, _, rarityValue):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Backdrop
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
case let .pattern(name, _, rarityValue):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Symbol
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
default:
|
||||
title = ""
|
||||
subtitle = ""
|
||||
|
||||
@@ -214,7 +214,7 @@ private final class GiftValueSheetContent: CombinedComponent {
|
||||
giftIconSubject = .starGift(gift: gift, price: "")
|
||||
case let .unique(gift):
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
if case let .model(_, file, _, _) = attribute {
|
||||
animationFile = file
|
||||
}
|
||||
}
|
||||
|
||||
+37
-27
@@ -234,8 +234,9 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
if self.testUpgradeAnimation {
|
||||
if gift.giftId != 0 {
|
||||
self.upgradePreviewDisposable.add((context.engine.payments.starGiftUpgradePreview(giftId: gift.giftId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] upgradePreview in
|
||||
let upgradePreviewSignal = context.engine.payments.starGiftUpgradePreview(giftId: gift.giftId)
|
||||
|> deliverOnMainQueue
|
||||
self.upgradePreviewDisposable.add(upgradePreviewSignal.start(next: { [weak self] upgradePreview in
|
||||
guard let self, let upgradePreview else {
|
||||
return
|
||||
}
|
||||
@@ -243,7 +244,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
for attribute in upgradePreview.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
case let .pattern(_, file, _):
|
||||
self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
@@ -261,8 +262,9 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
peerIds.append(releasedBy)
|
||||
}
|
||||
if arguments.canUpgrade || arguments.upgradeStars != nil || arguments.prepaidUpgradeHash != nil {
|
||||
self.upgradePreviewDisposable.add((context.engine.payments.starGiftUpgradePreview(giftId: gift.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] upgradePreview in
|
||||
let upgradePreviewSignal = context.engine.payments.starGiftUpgradePreview(giftId: gift.id)
|
||||
|> deliverOnMainQueue
|
||||
self.upgradePreviewDisposable.add(upgradePreviewSignal.start(next: { [weak self] upgradePreview in
|
||||
guard let self, let upgradePreview else {
|
||||
return
|
||||
}
|
||||
@@ -270,7 +272,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
for attribute in upgradePreview.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
case let .pattern(_, file, _):
|
||||
self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
@@ -501,7 +503,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
animationFile = gift.file
|
||||
case let .unique(gift):
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
if case let .model(_, file, _, _) = attribute {
|
||||
animationFile = file
|
||||
break
|
||||
}
|
||||
@@ -718,7 +720,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let context = self.context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let proceed = { [weak self, weak starsContext, weak controller] in
|
||||
let proceed: () -> Void = { [weak self, weak starsContext, weak controller] in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
@@ -745,7 +747,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
|
||||
self.subject = .profileGift(peerId, gift.withGift(.unique(uniqueGift.withAttributes(updatedAttributes))))
|
||||
case let .message(message):
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer) = action.action, case let .unique(uniqueGift) = gift {
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, _, _) = action.action, case let .unique(uniqueGift) = gift {
|
||||
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
|
||||
let updatedMedia: [Media] = [
|
||||
TelegramMediaAction(
|
||||
@@ -766,7 +768,9 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
canResaleDate: canResaleDate,
|
||||
dropOriginalDetailsStars: nil,
|
||||
assigned: assigned,
|
||||
fromOffer: fromOffer
|
||||
fromOffer: fromOffer,
|
||||
canCraftAt: nil,
|
||||
isCrafted: false
|
||||
)
|
||||
)
|
||||
]
|
||||
@@ -1008,7 +1012,10 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|> then(
|
||||
context.engine.payments.getUniqueStarGift(slug: gift.slug)
|
||||
|> map { gift in
|
||||
return gift?.themePeerId
|
||||
return gift.themePeerId
|
||||
}
|
||||
|> `catch` { _ -> Signal<EnginePeer.Id?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1376,6 +1383,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let variantsController = self.context.sharedContext.makeGiftUpgradeVariantsScreen(
|
||||
context: self.context,
|
||||
gift: arguments.gift,
|
||||
crafted: false,
|
||||
attributes: attributes,
|
||||
selectedAttributes: selectedAttributes,
|
||||
focusedAttribute: attribute
|
||||
@@ -1443,7 +1451,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
if case let .unique(gift) = arguments.gift, let resellAmount = gift.resellAmounts?.first, resellAmount.amount.value > 0 {
|
||||
if arguments.reference != nil || gift.owner.peerId == context.account.peerId {
|
||||
if arguments.reference != nil || gift.owner?.peerId == context.account.peerId {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
@@ -1703,7 +1711,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: presentationData.dateTimeFormat))"
|
||||
let recipientPeerId = self.recipientPeerId ?? self.context.account.peerId
|
||||
|
||||
let action: (CurrencyAmount.Currency) -> Void = { currency in
|
||||
let action: (CurrencyAmount.Currency) -> Void = { (currency: CurrencyAmount.Currency) in
|
||||
guard let resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == currency }) else {
|
||||
guard let controller = self.getController() as? GiftViewScreen else {
|
||||
return
|
||||
@@ -1802,10 +1810,10 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
var animationFile: TelegramMediaFile?
|
||||
for attribute in uniqueGift.attributes {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
animationFile = file
|
||||
break
|
||||
}
|
||||
if case let .model(_, file, _, _) = attribute {
|
||||
animationFile = file
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let navigationController = controller.navigationController as? NavigationController {
|
||||
@@ -3671,7 +3679,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if state.pendingWear {
|
||||
var fileId: Int64?
|
||||
for attribute in uniqueGift.attributes {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
if case let .model(_, file, _, _) = attribute {
|
||||
fileId = file.fileId.id
|
||||
}
|
||||
if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
|
||||
@@ -3780,6 +3788,8 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
)
|
||||
)
|
||||
))
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
if let peerId = uniqueGift.hostPeerId, let peer = state.peerMap[peerId] {
|
||||
@@ -4113,17 +4123,17 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
var otherValuesAndPercentages: [(value: String, percentage: Float)] = []
|
||||
|
||||
switch attribute {
|
||||
case let .model(name, _, rarity):
|
||||
case let .model(name, _, rarity, _):
|
||||
id = "model"
|
||||
title = strings.Gift_Unique_Model
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = state.modelButtonTag
|
||||
|
||||
if state.justUpgraded, let sampleAttributes = state.upgradePreview?.attributes {
|
||||
for sampleAttribute in sampleAttributes {
|
||||
if case let .model(name, _, rarity) = sampleAttribute {
|
||||
otherValuesAndPercentages.append((name, Float(rarity) * 0.1))
|
||||
if case let .model(name, _, rarity, _) = sampleAttribute {
|
||||
otherValuesAndPercentages.append((name, Float(rarity.permilleValue) * 0.1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4131,13 +4141,13 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
id = "backdrop"
|
||||
title = strings.Gift_Unique_Backdrop
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = state.backdropButtonTag
|
||||
|
||||
if state.justUpgraded, let sampleAttributes = state.upgradePreview?.attributes {
|
||||
for sampleAttribute in sampleAttributes {
|
||||
if case let .backdrop(name, _, _, _, _, _, rarity) = sampleAttribute {
|
||||
otherValuesAndPercentages.append((name, Float(rarity) * 0.1))
|
||||
otherValuesAndPercentages.append((name, Float(rarity.permilleValue) * 0.1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4145,13 +4155,13 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
id = "pattern"
|
||||
title = strings.Gift_Unique_Symbol
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = state.symbolButtonTag
|
||||
|
||||
if state.justUpgraded, let sampleAttributes = state.upgradePreview?.attributes {
|
||||
for sampleAttribute in sampleAttributes {
|
||||
if case let .pattern(name, _, rarity) = sampleAttribute {
|
||||
otherValuesAndPercentages.append((name, Float(rarity) * 0.1))
|
||||
otherValuesAndPercentages.append((name, Float(rarity.permilleValue) * 0.1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5500,7 +5510,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
|
||||
let fromPeerId = senderId ?? message.author?.id
|
||||
return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId, number)
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _):
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, _, _):
|
||||
var reference: StarGiftReference
|
||||
if let peerId, let savedId {
|
||||
reference = .peer(peerId: peerId, id: savedId)
|
||||
|
||||
Reference in New Issue
Block a user