mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Various improvements
This commit is contained in:
parent
4251ee2c1b
commit
fbccdd47df
@ -7711,5 +7711,6 @@ Sorry for the inconvenience.";
|
|||||||
"Premium.Purchase.ErrorUnknown" = "An error occurred. Please try again.";
|
"Premium.Purchase.ErrorUnknown" = "An error occurred. Please try again.";
|
||||||
"Premium.Purchase.ErrorNetwork" = "Please check your internet connection and try again.";
|
"Premium.Purchase.ErrorNetwork" = "Please check your internet connection and try again.";
|
||||||
"Premium.Purchase.ErrorNotAllowed" = "The device is not not allowed to make the payment.";
|
"Premium.Purchase.ErrorNotAllowed" = "The device is not not allowed to make the payment.";
|
||||||
|
"Premium.Purchase.ErrorCantMakePayments" = "In-app purchases are not allowed on this device.";
|
||||||
|
|
||||||
"Settings.Premium" = "Telegram Premium";
|
"Settings.Premium" = "Telegram Premium";
|
||||||
|
@ -2241,7 +2241,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: titleFrame.origin.y), size: titleFrame.size))
|
transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: titleFrame.origin.y), size: titleFrame.size))
|
||||||
|
|
||||||
let authorFrame = self.authorNode.frame
|
let authorFrame = self.authorNode.frame
|
||||||
transition.updateFrame(node: self.authorNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: authorFrame.origin.y), size: authorFrame.size))
|
transition.updateFrame(node: self.authorNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: authorFrame.origin.y), size: authorFrame.size))
|
||||||
|
|
||||||
transition.updateFrame(node: self.inputActivitiesNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: self.inputActivitiesNode.frame.minY), size: self.inputActivitiesNode.bounds.size))
|
transition.updateFrame(node: self.inputActivitiesNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: self.inputActivitiesNode.frame.minY), size: self.inputActivitiesNode.bounds.size))
|
||||||
|
|
||||||
@ -2253,7 +2253,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
transition.updateFrameAdditive(node: dustNode, frame: textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0))
|
transition.updateFrameAdditive(node: dustNode, frame: textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaPreviewOffsetX = textFrame.origin.x + 1.0
|
var mediaPreviewOffsetX = textFrame.origin.x
|
||||||
let contentImageSpacing: CGFloat = 2.0
|
let contentImageSpacing: CGFloat = 2.0
|
||||||
for (_, media, mediaSize) in self.currentMediaPreviewSpecs {
|
for (_, media, mediaSize) in self.currentMediaPreviewSpecs {
|
||||||
guard let mediaId = media.id else {
|
guard let mediaId = media.id else {
|
||||||
@ -2279,7 +2279,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mutedIconFrame = self.mutedIconNode.frame
|
let mutedIconFrame = self.mutedIconNode.frame
|
||||||
transition.updateFrame(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 4.0, y: contentRect.origin.y - 2.0), size: mutedIconFrame.size))
|
transition.updateFrame(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 5.0, y: mutedIconFrame.minY), size: mutedIconFrame.size))
|
||||||
nextTitleIconOrigin += mutedIconFrame.size.width + 3.0
|
nextTitleIconOrigin += mutedIconFrame.size.width + 3.0
|
||||||
|
|
||||||
let badgeFrame = self.badgeNode.frame
|
let badgeFrame = self.badgeNode.frame
|
||||||
|
@ -30,6 +30,8 @@ public final class InAppPurchaseManager: NSObject {
|
|||||||
case cancelled
|
case cancelled
|
||||||
case network
|
case network
|
||||||
case notAllowed
|
case notAllowed
|
||||||
|
case cantMakePayments
|
||||||
|
case assignFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RestoreState {
|
public enum RestoreState {
|
||||||
@ -51,6 +53,7 @@ public final class InAppPurchaseManager: NSObject {
|
|||||||
case restored(transactionId: String?)
|
case restored(transactionId: String?)
|
||||||
case purchasing
|
case purchasing
|
||||||
case failed(error: SKError?)
|
case failed(error: SKError?)
|
||||||
|
case assignFailed
|
||||||
case deferred
|
case deferred
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +66,7 @@ public final class InAppPurchaseManager: NSObject {
|
|||||||
|
|
||||||
private let stateQueue = Queue()
|
private let stateQueue = Queue()
|
||||||
private var paymentContexts: [String: PaymentTransactionContext] = [:]
|
private var paymentContexts: [String: PaymentTransactionContext] = [:]
|
||||||
|
|
||||||
private var onRestoreCompletion: ((RestoreState) -> Void)?
|
private var onRestoreCompletion: ((RestoreState) -> Void)?
|
||||||
|
|
||||||
private let disposableSet = DisposableDict<String>()
|
private let disposableSet = DisposableDict<String>()
|
||||||
@ -82,6 +85,10 @@ public final class InAppPurchaseManager: NSObject {
|
|||||||
SKPaymentQueue.default().remove(self)
|
SKPaymentQueue.default().remove(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var canMakePayments: Bool {
|
||||||
|
return SKPaymentQueue.canMakePayments()
|
||||||
|
}
|
||||||
|
|
||||||
private func requestProducts() {
|
private func requestProducts() {
|
||||||
guard !self.premiumProductId.isEmpty else {
|
guard !self.premiumProductId.isEmpty else {
|
||||||
return
|
return
|
||||||
@ -119,10 +126,16 @@ public final class InAppPurchaseManager: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func buyProduct(_ product: Product, account: Account) -> Signal<PurchaseState, PurchaseError> {
|
public func buyProduct(_ product: Product) -> Signal<PurchaseState, PurchaseError> {
|
||||||
Logger.shared.log("InAppPurchaseManager", "Buying product: \(product.skProduct.productIdentifier), price \(product.price)")
|
if !self.canMakePayments {
|
||||||
|
return .fail(.cantMakePayments)
|
||||||
|
}
|
||||||
|
let accountPeerId = "\(self.engine.account.peerId.toInt64())"
|
||||||
|
|
||||||
let payment = SKPayment(product: product.skProduct)
|
Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)")
|
||||||
|
|
||||||
|
let payment = SKMutablePayment(product: product.skProduct)
|
||||||
|
payment.applicationUsername = accountPeerId
|
||||||
SKPaymentQueue.default().add(payment)
|
SKPaymentQueue.default().add(payment)
|
||||||
|
|
||||||
let productIdentifier = payment.productIdentifier
|
let productIdentifier = payment.productIdentifier
|
||||||
@ -156,6 +169,8 @@ public final class InAppPurchaseManager: NSObject {
|
|||||||
} else {
|
} else {
|
||||||
subscriber.putError(.generic)
|
subscriber.putError(.generic)
|
||||||
}
|
}
|
||||||
|
case .assignFailed:
|
||||||
|
subscriber.putError(.assignFailed)
|
||||||
case .deferred, .purchasing:
|
case .deferred, .purchasing:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -205,39 +220,48 @@ private func getReceiptData() -> Data? {
|
|||||||
extension InAppPurchaseManager: SKPaymentTransactionObserver {
|
extension InAppPurchaseManager: SKPaymentTransactionObserver {
|
||||||
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
|
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
|
||||||
for transaction in transactions {
|
for transaction in transactions {
|
||||||
|
let accountPeerId = "\(self.engine.account.peerId.toInt64())"
|
||||||
|
if let applicationUsername = transaction.payment.applicationUsername, applicationUsername != accountPeerId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
let productIdentifier = transaction.payment.productIdentifier
|
let productIdentifier = transaction.payment.productIdentifier
|
||||||
self.stateQueue.async {
|
self.stateQueue.async {
|
||||||
let transactionState: TransactionState?
|
let transactionState: TransactionState?
|
||||||
switch transaction.transactionState {
|
switch transaction.transactionState {
|
||||||
case .purchased:
|
case .purchased:
|
||||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased")
|
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased")
|
||||||
let transactionIdentifier = transaction.transactionIdentifier
|
let transactionIdentifier = transaction.transactionIdentifier
|
||||||
transactionState = .purchased(transactionId: transactionIdentifier)
|
transactionState = .purchased(transactionId: transactionIdentifier)
|
||||||
if let transactionIdentifier = transactionIdentifier {
|
if let transactionIdentifier = transactionIdentifier {
|
||||||
self.disposableSet.set(
|
self.disposableSet.set(
|
||||||
self.engine.payments.sendAppStoreReceipt(receipt: getReceiptData() ?? Data(), restore: false).start(error: { _ in
|
self.engine.payments.sendAppStoreReceipt(receipt: getReceiptData() ?? Data(), restore: false).start(error: { [weak self] _ in
|
||||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") failed to assign AppStore transaction")
|
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") failed to assign AppStore transaction")
|
||||||
queue.finishTransaction(transaction)
|
queue.finishTransaction(transaction)
|
||||||
|
|
||||||
|
if let strongSelf = self, let context = strongSelf.paymentContexts[productIdentifier] {
|
||||||
|
context.subscriber(.assignFailed)
|
||||||
|
}
|
||||||
}, completed: {
|
}, completed: {
|
||||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") successfully assigned AppStore transaction")
|
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") successfully assigned AppStore transaction")
|
||||||
queue.finishTransaction(transaction)
|
queue.finishTransaction(transaction)
|
||||||
}),
|
}),
|
||||||
forKey: transactionIdentifier
|
forKey: transactionIdentifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case .restored:
|
case .restored:
|
||||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "") restroring")
|
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "") restroring")
|
||||||
let transactionIdentifier = transaction.transactionIdentifier
|
let transactionIdentifier = transaction.transactionIdentifier
|
||||||
transactionState = .restored(transactionId: transactionIdentifier)
|
transactionState = .restored(transactionId: transactionIdentifier)
|
||||||
case .failed:
|
case .failed:
|
||||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") failed \((transaction.error as? SKError)?.localizedDescription ?? "")")
|
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") failed \((transaction.error as? SKError)?.localizedDescription ?? "")")
|
||||||
transactionState = .failed(error: transaction.error as? SKError)
|
transactionState = .failed(error: transaction.error as? SKError)
|
||||||
queue.finishTransaction(transaction)
|
queue.finishTransaction(transaction)
|
||||||
case .purchasing:
|
case .purchasing:
|
||||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") purchasing")
|
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") purchasing")
|
||||||
transactionState = .purchasing
|
transactionState = .purchasing
|
||||||
case .deferred:
|
case .deferred:
|
||||||
Logger.shared.log("InAppPurchaseManager", "Transaction \(transaction.transactionIdentifier ?? "") deferred")
|
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") deferred")
|
||||||
transactionState = .deferred
|
transactionState = .deferred
|
||||||
default:
|
default:
|
||||||
transactionState = nil
|
transactionState = nil
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
//import Foundation
|
||||||
|
//import UIKit
|
||||||
|
//import SwiftSignalKit
|
||||||
|
//import Postbox
|
||||||
|
//import TelegramCore
|
||||||
|
//import TelegramUIPreferences
|
||||||
|
//
|
||||||
|
//final class StoredTransactionState: Codable {
|
||||||
|
// let timestamp: Double
|
||||||
|
// let playbackRate: AudioPlaybackRate
|
||||||
|
//
|
||||||
|
// init(timestamp: Double, playbackRate: AudioPlaybackRate) {
|
||||||
|
// self.timestamp = timestamp
|
||||||
|
// self.playbackRate = playbackRate
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public init(from decoder: Decoder) throws {
|
||||||
|
// let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||||
|
//
|
||||||
|
// self.timestamp = try container.decode(Double.self, forKey: "timestamp")
|
||||||
|
// self.playbackRate = AudioPlaybackRate(rawValue: try container.decode(Int32.self, forKey: "playbackRate")) ?? .x1
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public func encode(to encoder: Encoder) throws {
|
||||||
|
// var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||||
|
//
|
||||||
|
// try container.encode(self.timestamp, forKey: "timestamp")
|
||||||
|
// try container.encode(self.playbackRate.rawValue, forKey: "playbackRate")
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//public func storedState(engine: TelegramEngine, : MessageId) -> Signal<MediaPlaybackStoredState?, NoError> {
|
||||||
|
// let key = ValueBoxKey(length: 20)
|
||||||
|
// key.setInt32(0, value: messageId.namespace)
|
||||||
|
// key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
|
||||||
|
// key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
|
||||||
|
// key.setInt32(16, value: messageId.id)
|
||||||
|
//
|
||||||
|
// return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key))
|
||||||
|
// |> map { entry -> MediaPlaybackStoredState? in
|
||||||
|
// return entry?.get(MediaPlaybackStoredState.self)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//public func updateMediaPlaybackStoredStateInteractively(engine: TelegramEngine, messageId: MessageId, state: MediaPlaybackStoredState?) -> Signal<Never, NoError> {
|
||||||
|
// let key = ValueBoxKey(length: 20)
|
||||||
|
// key.setInt32(0, value: messageId.namespace)
|
||||||
|
// key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
|
||||||
|
// key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
|
||||||
|
// key.setInt32(16, value: messageId.id)
|
||||||
|
//
|
||||||
|
// if let state = state {
|
||||||
|
// return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key, item: state)
|
||||||
|
// } else {
|
||||||
|
// return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.mediaPlaybackStoredState, id: key)
|
||||||
|
// }
|
||||||
|
//}
|
@ -1,4 +1,44 @@
|
|||||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
load(
|
||||||
|
"@build_bazel_rules_apple//apple:resources.bzl",
|
||||||
|
"apple_resource_bundle",
|
||||||
|
"apple_resource_group",
|
||||||
|
)
|
||||||
|
load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||||
|
"plist_fragment",
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "PremiumUIMetalResources",
|
||||||
|
srcs = glob([
|
||||||
|
"MetalResources/**/*.*",
|
||||||
|
]),
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
plist_fragment(
|
||||||
|
name = "PremiumUIBundleInfoPlist",
|
||||||
|
extension = "plist",
|
||||||
|
template =
|
||||||
|
"""
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.telegram.PremiumUI</string>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>PremiumUI</string>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
apple_resource_bundle(
|
||||||
|
name = "PremiumUIBundle",
|
||||||
|
infoplists = [
|
||||||
|
":PremiumUIBundleInfoPlist",
|
||||||
|
],
|
||||||
|
resources = [
|
||||||
|
":PremiumUIMetalResources",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "PremiumUIResources",
|
name = "PremiumUIResources",
|
||||||
@ -17,6 +57,9 @@ swift_library(
|
|||||||
copts = [
|
copts = [
|
||||||
"-warnings-as-errors",
|
"-warnings-as-errors",
|
||||||
],
|
],
|
||||||
|
data = [
|
||||||
|
":PremiumUIBundle",
|
||||||
|
],
|
||||||
deps = [
|
deps = [
|
||||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
|
BIN
submodules/PremiumUI/MetalResources/chars.png
Normal file
BIN
submodules/PremiumUI/MetalResources/chars.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
79
submodules/PremiumUI/MetalResources/matrix.metal
Normal file
79
submodules/PremiumUI/MetalResources/matrix.metal
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#include <metal_stdlib>
|
||||||
|
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
packed_float2 position;
|
||||||
|
} Vertex;
|
||||||
|
|
||||||
|
struct RasterizerData
|
||||||
|
{
|
||||||
|
float4 position [[position]];
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex RasterizerData matrixVertex
|
||||||
|
(
|
||||||
|
constant Vertex *vertexArray[[buffer(0)]],
|
||||||
|
uint vertexID [[ vertex_id ]]
|
||||||
|
) {
|
||||||
|
RasterizerData out;
|
||||||
|
|
||||||
|
out.position = vector_float4(vertexArray[vertexID].position[0], vertexArray[vertexID].position[1], 0.0, 1.0);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
float text(float2 uvIn,
|
||||||
|
texture2d<half> symbolTexture,
|
||||||
|
texture2d<float> noiseTexture,
|
||||||
|
float time)
|
||||||
|
{
|
||||||
|
constexpr sampler textureSampler(min_filter::linear, mag_filter::linear, mip_filter::linear, address::repeat);
|
||||||
|
|
||||||
|
float count = 32.0;
|
||||||
|
|
||||||
|
float2 noiseResolution = float2(256.0, 256.0);
|
||||||
|
|
||||||
|
float2 uv = fmod(uvIn, 1.0 / count) * count;
|
||||||
|
float2 block = uvIn * count - uv;
|
||||||
|
uv = uv * 0.8 + 0.1;
|
||||||
|
uv += floor(noiseTexture.sample(textureSampler, block / noiseResolution + time * .00025).xy * 256.);
|
||||||
|
uv *= -1.0;
|
||||||
|
|
||||||
|
uv *= 0.25;
|
||||||
|
|
||||||
|
return symbolTexture.sample(textureSampler, uv).g;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 rain(float2 uvIn,
|
||||||
|
uint2 resolution,
|
||||||
|
float time)
|
||||||
|
{
|
||||||
|
float count = 32.0;
|
||||||
|
uvIn.x -= fmod(uvIn.x, 1.0 / count);
|
||||||
|
uvIn.y -= fmod(uvIn.y, 1.0 / count);
|
||||||
|
|
||||||
|
float2 fragCoord = uvIn * float2(resolution);
|
||||||
|
|
||||||
|
float offset = sin(fragCoord.x * 15.0);
|
||||||
|
float speed = cos(fragCoord.x * 3.0) * 0.3 + 0.7;
|
||||||
|
|
||||||
|
float y = fract(fragCoord.y / resolution.y + time * speed + offset);
|
||||||
|
|
||||||
|
return float4(1.0, 1.0, 1.0, 1.0 / (y * 30.0) - 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment half4 matrixFragment(RasterizerData in[[stage_in]],
|
||||||
|
texture2d<half> symbolTexture [[ texture(0) ]],
|
||||||
|
texture2d<float> noiseTexture [[ texture(1) ]],
|
||||||
|
constant uint2 &resolution[[buffer(0)]],
|
||||||
|
constant float &time[[buffer(1)]])
|
||||||
|
{
|
||||||
|
float2 uv = (in.position.xy / float2(resolution.xy) - float2(0.5, 0.5));
|
||||||
|
uv.y -= 0.1;
|
||||||
|
|
||||||
|
float2 lookup = float2(0.08 / (uv.x), (0.9 - abs(uv.x)) * uv.y * -1.0) * 2.0;
|
||||||
|
|
||||||
|
float4 out = text(lookup, symbolTexture, noiseTexture, time) * rain(lookup, resolution, time);
|
||||||
|
return half4(out);
|
||||||
|
}
|
BIN
submodules/PremiumUI/MetalResources/random.jpg
Normal file
BIN
submodules/PremiumUI/MetalResources/random.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
Binary file not shown.
BIN
submodules/PremiumUI/Resources/swirl.scn
Normal file
BIN
submodules/PremiumUI/Resources/swirl.scn
Normal file
Binary file not shown.
59
submodules/PremiumUI/Sources/BadgeStarsView.swift
Normal file
59
submodules/PremiumUI/Sources/BadgeStarsView.swift
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import SceneKit
|
||||||
|
import Display
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
|
final class BadgeStarsView: UIView, PhoneDemoDecorationView {
|
||||||
|
private let sceneView: SCNView
|
||||||
|
|
||||||
|
private var leftParticles: SCNNode?
|
||||||
|
private var rightParticles: SCNNode?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
|
||||||
|
self.sceneView.backgroundColor = .clear
|
||||||
|
if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") {
|
||||||
|
self.sceneView.scene = try? SCNScene(url: url, options: nil)
|
||||||
|
}
|
||||||
|
self.sceneView.isUserInteractionEnabled = false
|
||||||
|
self.sceneView.preferredFramesPerSecond = 60
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.alpha = 0.0
|
||||||
|
|
||||||
|
self.addSubview(self.sceneView)
|
||||||
|
|
||||||
|
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
|
||||||
|
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVisible(_ visible: Bool) {
|
||||||
|
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
|
||||||
|
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
|
||||||
|
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
||||||
|
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
|
||||||
|
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
|
||||||
|
strongSelf.leftParticles?.removeFromParentNode()
|
||||||
|
strongSelf.rightParticles?.removeFromParentNode()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetAnimation() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
|
||||||
|
}
|
||||||
|
}
|
198
submodules/PremiumUI/Sources/DataRainView.swift
Normal file
198
submodules/PremiumUI/Sources/DataRainView.swift
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import Foundation
|
||||||
|
import Metal
|
||||||
|
import MetalKit
|
||||||
|
import Display
|
||||||
|
|
||||||
|
@available(iOS 10.0, *)
|
||||||
|
public final class MatrixView: MTKView, MTKViewDelegate, PhoneDemoDecorationView {
|
||||||
|
public func draw(in view: MTKView) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private let commandQueue: MTLCommandQueue
|
||||||
|
private let drawPassthroughPipelineState: MTLRenderPipelineState
|
||||||
|
|
||||||
|
private var displayLink: CADisplayLink?
|
||||||
|
|
||||||
|
// private var metalLayer: CAMetalLayer {
|
||||||
|
// return self.layer as! CAMetalLayer
|
||||||
|
// }
|
||||||
|
|
||||||
|
private let symbolTexture: MTLTexture
|
||||||
|
private let randomTexture: MTLTexture
|
||||||
|
|
||||||
|
private var viewportDimensions = CGSize(width: 1, height: 1)
|
||||||
|
|
||||||
|
private var startTimestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
|
public init?(test: Bool) {
|
||||||
|
let mainBundle = Bundle(for: MatrixView.self)
|
||||||
|
|
||||||
|
guard let path = mainBundle.path(forResource: "PremiumUIBundle", ofType: "bundle") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let bundle = Bundle(path: path) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let device = MTLCreateSystemDefaultDevice() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let commandQueue = device.makeCommandQueue() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.commandQueue = commandQueue
|
||||||
|
|
||||||
|
guard let loadedVertexProgram = defaultLibrary.makeFunction(name: "matrixVertex") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let loadedFragmentProgram = defaultLibrary.makeFunction(name: "matrixFragment") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let textureLoader = MTKTextureLoader(device: device)
|
||||||
|
|
||||||
|
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
pipelineStateDescriptor.vertexFunction = loadedVertexProgram
|
||||||
|
pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram
|
||||||
|
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
|
||||||
|
pipelineStateDescriptor.colorAttachments[0].isBlendingEnabled = true
|
||||||
|
pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add
|
||||||
|
pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add
|
||||||
|
pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
|
||||||
|
pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
|
||||||
|
pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||||
|
pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||||
|
|
||||||
|
self.drawPassthroughPipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
|
||||||
|
|
||||||
|
guard let url = bundle.url(forResource: "chars", withExtension: "png"), let texture = try? textureLoader.newTexture(URL: url, options: nil) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.symbolTexture = texture
|
||||||
|
|
||||||
|
guard let url = bundle.url(forResource: "random", withExtension: "jpg"), let texture = try? textureLoader.newTexture(URL: url, options: nil) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.randomTexture = texture
|
||||||
|
|
||||||
|
super.init(frame: CGRect(), device: device)
|
||||||
|
|
||||||
|
self.delegate = self
|
||||||
|
|
||||||
|
self.isOpaque = false
|
||||||
|
self.backgroundColor = .clear
|
||||||
|
|
||||||
|
self.framebufferOnly = true
|
||||||
|
|
||||||
|
class DisplayLinkProxy: NSObject {
|
||||||
|
weak var target: MatrixView?
|
||||||
|
init(target: MatrixView) {
|
||||||
|
self.target = target
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func displayLinkEvent() {
|
||||||
|
self.target?.displayLinkEvent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 60.0, preferred: 60.0)
|
||||||
|
}
|
||||||
|
self.displayLink?.add(to: .main, forMode: .common)
|
||||||
|
self.displayLink?.isPaused = false
|
||||||
|
|
||||||
|
self.isPaused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
||||||
|
self.viewportDimensions = size
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.displayLink?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVisible(_ visible: Bool) {
|
||||||
|
if visible {
|
||||||
|
self.displayLink?.isPaused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
||||||
|
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.4 : 0.0, completion: { [weak self] finished in
|
||||||
|
if let strongSelf = self, finished && !visible {
|
||||||
|
strongSelf.displayLink?.isPaused = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetAnimation() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func displayLinkEvent() {
|
||||||
|
self.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func draw(_ rect: CGRect) {
|
||||||
|
self.redraw(drawable: self.currentDrawable!)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func redraw(drawable: MTLDrawable) {
|
||||||
|
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderPassDescriptor = self.currentRenderPassDescriptor!
|
||||||
|
renderPassDescriptor.colorAttachments[0].loadAction = .clear
|
||||||
|
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0)
|
||||||
|
|
||||||
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewportDimensions = self.viewportDimensions
|
||||||
|
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: viewportDimensions.width, height: viewportDimensions.height, znear: -1.0, zfar: 1.0))
|
||||||
|
|
||||||
|
renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState)
|
||||||
|
|
||||||
|
var vertices: [Float] = [
|
||||||
|
1, -1,
|
||||||
|
-1, -1,
|
||||||
|
-1, 1,
|
||||||
|
1, -1,
|
||||||
|
-1, 1,
|
||||||
|
1, 1
|
||||||
|
]
|
||||||
|
renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
|
||||||
|
|
||||||
|
renderEncoder.setFragmentTexture(self.symbolTexture, index: 0)
|
||||||
|
renderEncoder.setFragmentTexture(self.randomTexture, index: 1)
|
||||||
|
|
||||||
|
var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height))
|
||||||
|
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||||
|
|
||||||
|
var time = Float(CACurrentMediaTime() - self.startTimestamp) * 0.75
|
||||||
|
renderEncoder.setFragmentBytes(&time, length: 4, index: 1)
|
||||||
|
|
||||||
|
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
|
||||||
|
|
||||||
|
renderEncoder.endEncoding()
|
||||||
|
|
||||||
|
commandBuffer.present(drawable)
|
||||||
|
commandBuffer.commit()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
107
submodules/PremiumUI/Sources/FasterStarsView.swift
Normal file
107
submodules/PremiumUI/Sources/FasterStarsView.swift
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import SceneKit
|
||||||
|
import Display
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
|
final class FasterStarsView: UIView, PhoneDemoDecorationView {
|
||||||
|
private let sceneView: SCNView
|
||||||
|
|
||||||
|
private var particles: SCNNode?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
|
||||||
|
self.sceneView.backgroundColor = .clear
|
||||||
|
if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") {
|
||||||
|
self.sceneView.scene = try? SCNScene(url: url, options: nil)
|
||||||
|
}
|
||||||
|
self.sceneView.isUserInteractionEnabled = false
|
||||||
|
self.sceneView.preferredFramesPerSecond = 60
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.alpha = 0.0
|
||||||
|
|
||||||
|
self.addSubview(self.sceneView)
|
||||||
|
|
||||||
|
self.particles = self.sceneView.scene?.rootNode.childNode(withName: "particles", recursively: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.particles = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVisible(_ visible: Bool) {
|
||||||
|
if visible, let particles = self.particles, particles.parent == nil {
|
||||||
|
self.sceneView.scene?.rootNode.addChildNode(particles)
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
||||||
|
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.4 : 0.0, completion: { [weak self] finished in
|
||||||
|
if let strongSelf = self, finished && !visible && strongSelf.particles?.parent != nil {
|
||||||
|
strongSelf.particles?.removeFromParentNode()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playing = false
|
||||||
|
func startAnimation() {
|
||||||
|
guard !self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.playing = true
|
||||||
|
|
||||||
|
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
|
||||||
|
speedAnimation.fromValue = 1.0
|
||||||
|
speedAnimation.toValue = 1.8
|
||||||
|
speedAnimation.duration = 0.8
|
||||||
|
speedAnimation.fillMode = .forwards
|
||||||
|
particles.addAnimation(speedAnimation, forKey: "speedFactor")
|
||||||
|
|
||||||
|
particles.speedFactor = 3.0
|
||||||
|
|
||||||
|
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
|
||||||
|
stretchAnimation.fromValue = 0.05
|
||||||
|
stretchAnimation.toValue = 0.3
|
||||||
|
stretchAnimation.duration = 0.8
|
||||||
|
stretchAnimation.fillMode = .forwards
|
||||||
|
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
|
||||||
|
|
||||||
|
particles.stretchFactor = 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetAnimation() {
|
||||||
|
guard self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.playing = false
|
||||||
|
|
||||||
|
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
|
||||||
|
speedAnimation.fromValue = 3.0
|
||||||
|
speedAnimation.toValue = 1.0
|
||||||
|
speedAnimation.duration = 0.35
|
||||||
|
speedAnimation.fillMode = .forwards
|
||||||
|
particles.addAnimation(speedAnimation, forKey: "speedFactor")
|
||||||
|
|
||||||
|
particles.speedFactor = 1.0
|
||||||
|
|
||||||
|
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
|
||||||
|
stretchAnimation.fromValue = 0.3
|
||||||
|
stretchAnimation.toValue = 0.05
|
||||||
|
stretchAnimation.duration = 0.35
|
||||||
|
stretchAnimation.fillMode = .forwards
|
||||||
|
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
|
||||||
|
|
||||||
|
particles.stretchFactor = 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
|
||||||
|
}
|
||||||
|
}
|
@ -280,125 +280,9 @@ private final class PhoneView: UIView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class FasterStarsView: UIView {
|
protocol PhoneDemoDecorationView: UIView {
|
||||||
private let sceneView: SCNView
|
func setVisible(_ visible: Bool)
|
||||||
|
func resetAnimation()
|
||||||
override init(frame: CGRect) {
|
|
||||||
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
|
|
||||||
self.sceneView.backgroundColor = .clear
|
|
||||||
if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") {
|
|
||||||
self.sceneView.scene = try? SCNScene(url: url, options: nil)
|
|
||||||
}
|
|
||||||
self.sceneView.isUserInteractionEnabled = false
|
|
||||||
self.sceneView.preferredFramesPerSecond = 60
|
|
||||||
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
self.alpha = 0.0
|
|
||||||
|
|
||||||
self.addSubview(self.sceneView)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setVisible(_ visible: Bool) {
|
|
||||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
|
||||||
transition.updateAlpha(layer: self.layer, alpha: visible ? 1.0 : 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var playing = false
|
|
||||||
func startAnimation() {
|
|
||||||
guard !self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.playing = true
|
|
||||||
|
|
||||||
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
|
|
||||||
speedAnimation.fromValue = 1.0
|
|
||||||
speedAnimation.toValue = 1.8
|
|
||||||
speedAnimation.duration = 0.8
|
|
||||||
speedAnimation.fillMode = .forwards
|
|
||||||
particles.addAnimation(speedAnimation, forKey: "speedFactor")
|
|
||||||
|
|
||||||
particles.speedFactor = 3.0
|
|
||||||
|
|
||||||
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
|
|
||||||
stretchAnimation.fromValue = 0.05
|
|
||||||
stretchAnimation.toValue = 0.3
|
|
||||||
stretchAnimation.duration = 0.8
|
|
||||||
stretchAnimation.fillMode = .forwards
|
|
||||||
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
|
|
||||||
|
|
||||||
particles.stretchFactor = 0.3
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopAnimation() {
|
|
||||||
guard self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.playing = false
|
|
||||||
|
|
||||||
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
|
|
||||||
speedAnimation.fromValue = 3.0
|
|
||||||
speedAnimation.toValue = 1.0
|
|
||||||
speedAnimation.duration = 0.35
|
|
||||||
speedAnimation.fillMode = .forwards
|
|
||||||
particles.addAnimation(speedAnimation, forKey: "speedFactor")
|
|
||||||
|
|
||||||
particles.speedFactor = 1.0
|
|
||||||
|
|
||||||
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
|
|
||||||
stretchAnimation.fromValue = 0.3
|
|
||||||
stretchAnimation.toValue = 0.05
|
|
||||||
stretchAnimation.duration = 0.35
|
|
||||||
stretchAnimation.fillMode = .forwards
|
|
||||||
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
|
|
||||||
|
|
||||||
particles.stretchFactor = 0.05
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class BadgeStarsView: UIView {
|
|
||||||
private let sceneView: SCNView
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
|
|
||||||
self.sceneView.backgroundColor = .clear
|
|
||||||
if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") {
|
|
||||||
self.sceneView.scene = try? SCNScene(url: url, options: nil)
|
|
||||||
}
|
|
||||||
self.sceneView.isUserInteractionEnabled = false
|
|
||||||
self.sceneView.preferredFramesPerSecond = 60
|
|
||||||
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
self.alpha = 0.0
|
|
||||||
|
|
||||||
self.addSubview(self.sceneView)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setVisible(_ visible: Bool) {
|
|
||||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
|
||||||
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.75 : 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PhoneDemoComponent: Component {
|
final class PhoneDemoComponent: Component {
|
||||||
@ -411,6 +295,8 @@ final class PhoneDemoComponent: Component {
|
|||||||
|
|
||||||
enum BackgroundDecoration {
|
enum BackgroundDecoration {
|
||||||
case none
|
case none
|
||||||
|
case dataRain
|
||||||
|
case swirlStars
|
||||||
case fasterStars
|
case fasterStars
|
||||||
case badgeStars
|
case badgeStars
|
||||||
}
|
}
|
||||||
@ -462,14 +348,12 @@ final class PhoneDemoComponent: Component {
|
|||||||
private var isCentral = false
|
private var isCentral = false
|
||||||
private var component: PhoneDemoComponent?
|
private var component: PhoneDemoComponent?
|
||||||
|
|
||||||
private let starsContainerView: UIView
|
private let decorationContainerView: UIView
|
||||||
|
private var decorationView: PhoneDemoDecorationView?
|
||||||
private let containerView: UIView
|
private let containerView: UIView
|
||||||
private let phoneView: PhoneView
|
private let phoneView: PhoneView
|
||||||
|
|
||||||
private var fasterStarsView: FasterStarsView?
|
private var playbackStatusDisposable: Disposable?
|
||||||
private var badgeStarsView: BadgeStarsView?
|
|
||||||
|
|
||||||
private var starsDisposable: Disposable?
|
|
||||||
|
|
||||||
public var ready: Signal<Bool, NoError> {
|
public var ready: Signal<Bool, NoError> {
|
||||||
if let videoNode = self.phoneView.videoNode {
|
if let videoNode = self.phoneView.videoNode {
|
||||||
@ -483,8 +367,8 @@ final class PhoneDemoComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
self.starsContainerView = UIView(frame: frame)
|
self.decorationContainerView = UIView(frame: frame)
|
||||||
self.starsContainerView.clipsToBounds = true
|
self.decorationContainerView.clipsToBounds = true
|
||||||
|
|
||||||
self.containerView = UIView(frame: frame)
|
self.containerView = UIView(frame: frame)
|
||||||
self.containerView.clipsToBounds = true
|
self.containerView.clipsToBounds = true
|
||||||
@ -493,7 +377,7 @@ final class PhoneDemoComponent: Component {
|
|||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.addSubview(self.starsContainerView)
|
self.addSubview(self.decorationContainerView)
|
||||||
self.addSubview(self.containerView)
|
self.addSubview(self.containerView)
|
||||||
self.containerView.addSubview(self.phoneView)
|
self.containerView.addSubview(self.phoneView)
|
||||||
}
|
}
|
||||||
@ -502,42 +386,60 @@ final class PhoneDemoComponent: Component {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
// deinit {
|
||||||
self.starsDisposable?.dispose()
|
// self.playbackStatusDisposable?.dispose()
|
||||||
}
|
// }
|
||||||
|
|
||||||
public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
||||||
self.component = component
|
self.component = component
|
||||||
|
|
||||||
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
|
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
|
||||||
self.starsContainerView.frame = CGRect(origin: CGPoint(x: -availableSize.width * 0.5, y: 0.0), size: CGSize(width: availableSize.width * 2.0, height: availableSize.height))
|
self.decorationContainerView.frame = CGRect(origin: CGPoint(x: -availableSize.width * 0.5, y: 0.0), size: CGSize(width: availableSize.width * 2.0, height: availableSize.height))
|
||||||
self.phoneView.bounds = CGRect(origin: .zero, size: phoneSize)
|
self.phoneView.bounds = CGRect(origin: .zero, size: phoneSize)
|
||||||
|
|
||||||
switch component.decoration {
|
switch component.decoration {
|
||||||
case .none:
|
case .none:
|
||||||
break
|
break
|
||||||
|
case .dataRain:
|
||||||
|
if #available(iOS 10.0, *) {
|
||||||
|
if let _ = self.decorationView as? MatrixView {
|
||||||
|
} else if let rainView = MatrixView(test: true) {
|
||||||
|
rainView.frame = self.decorationContainerView.bounds.insetBy(dx: availableSize.width * 0.5, dy: 0.0)
|
||||||
|
self.decorationView = rainView
|
||||||
|
self.decorationContainerView.addSubview(rainView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .swirlStars:
|
||||||
|
if let _ = self.decorationView as? SwirlStarsView {
|
||||||
|
} else {
|
||||||
|
let starsView = SwirlStarsView(frame: self.decorationContainerView.bounds)
|
||||||
|
self.decorationView = starsView
|
||||||
|
self.decorationContainerView.addSubview(starsView)
|
||||||
|
}
|
||||||
case .fasterStars:
|
case .fasterStars:
|
||||||
if self.fasterStarsView == nil {
|
if let _ = self.decorationView as? FasterStarsView {
|
||||||
let starsView = FasterStarsView(frame: self.starsContainerView.bounds)
|
} else {
|
||||||
self.fasterStarsView = starsView
|
let starsView = FasterStarsView(frame: self.decorationContainerView.bounds)
|
||||||
self.starsContainerView.addSubview(starsView)
|
self.decorationView = starsView
|
||||||
|
self.decorationContainerView.addSubview(starsView)
|
||||||
|
|
||||||
self.starsDisposable = (self.phoneView.playbackStatus
|
self.playbackStatusDisposable = (self.phoneView.playbackStatus
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|> deliverOnMainQueue).start(next: { [weak starsView] status in
|
||||||
if let strongSelf = self, let status = status {
|
if let starsView = starsView, let status = status {
|
||||||
if status.timestamp > 8.0 {
|
if status.timestamp > 8.0 {
|
||||||
strongSelf.fasterStarsView?.stopAnimation()
|
starsView.resetAnimation()
|
||||||
} else if status.timestamp > 0.85 {
|
} else if status.timestamp > 0.85 {
|
||||||
strongSelf.fasterStarsView?.startAnimation()
|
starsView.startAnimation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case .badgeStars:
|
case .badgeStars:
|
||||||
if self.badgeStarsView == nil {
|
if let _ = self.decorationView as? BadgeStarsView {
|
||||||
let starsView = BadgeStarsView(frame: self.starsContainerView.bounds)
|
} else {
|
||||||
self.badgeStarsView = starsView
|
let starsView = BadgeStarsView(frame: self.decorationContainerView.bounds)
|
||||||
self.starsContainerView.addSubview(starsView)
|
self.decorationView = starsView
|
||||||
|
self.decorationContainerView.addSubview(starsView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,8 +463,9 @@ final class PhoneDemoComponent: Component {
|
|||||||
let isCentral = environment[DemoPageEnvironment.self].isCentral
|
let isCentral = environment[DemoPageEnvironment.self].isCentral
|
||||||
self.isCentral = isCentral
|
self.isCentral = isCentral
|
||||||
|
|
||||||
self.fasterStarsView?.setVisible(isVisible && abs(mappedPosition) < 0.4)
|
if let decorationView = self.decorationView {
|
||||||
self.badgeStarsView?.setVisible(isVisible && abs(mappedPosition) < 0.4)
|
decorationView.setVisible(isVisible && abs(mappedPosition) < 0.4)
|
||||||
|
}
|
||||||
|
|
||||||
self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + phoneX, y: phoneY)
|
self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + phoneX, y: phoneY)
|
||||||
self.phoneView.screenRotation = mappedPosition * -0.7
|
self.phoneView.screenRotation = mappedPosition * -0.7
|
||||||
@ -575,7 +478,7 @@ final class PhoneDemoComponent: Component {
|
|||||||
self.phoneView.play()
|
self.phoneView.play()
|
||||||
} else if !isVisible {
|
} else if !isVisible {
|
||||||
self.phoneView.reset()
|
self.phoneView.reset()
|
||||||
self.fasterStarsView?.stopAnimation()
|
self.decorationView?.resetAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
|
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
|
||||||
|
@ -725,7 +725,8 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .bottom,
|
position: .bottom,
|
||||||
videoFile: configuration.videos["more_upload"]
|
videoFile: configuration.videos["more_upload"],
|
||||||
|
decoration: .dataRain
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_UploadSize,
|
title: strings.Premium_UploadSize,
|
||||||
text: strings.Premium_UploadSizeInfo,
|
text: strings.Premium_UploadSizeInfo,
|
||||||
@ -760,7 +761,8 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["voice_to_text"]
|
videoFile: configuration.videos["voice_to_text"],
|
||||||
|
decoration: .badgeStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_VoiceToText,
|
title: strings.Premium_VoiceToText,
|
||||||
text: strings.Premium_VoiceToTextInfo,
|
text: strings.Premium_VoiceToTextInfo,
|
||||||
@ -777,7 +779,8 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .bottom,
|
position: .bottom,
|
||||||
videoFile: configuration.videos["no_ads"]
|
videoFile: configuration.videos["no_ads"],
|
||||||
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_NoAds,
|
title: strings.Premium_NoAds,
|
||||||
text: isStandalone ? strings.Premium_NoAdsStandaloneInfo : strings.Premium_NoAdsInfo,
|
text: isStandalone ? strings.Premium_NoAdsStandaloneInfo : strings.Premium_NoAdsInfo,
|
||||||
@ -831,7 +834,8 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["advanced_chat_management"]
|
videoFile: configuration.videos["advanced_chat_management"],
|
||||||
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_ChatManagement,
|
title: strings.Premium_ChatManagement,
|
||||||
text: strings.Premium_ChatManagementInfo,
|
text: strings.Premium_ChatManagementInfo,
|
||||||
@ -866,7 +870,8 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["animated_userpics"]
|
videoFile: configuration.videos["animated_userpics"],
|
||||||
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_Avatar,
|
title: strings.Premium_Avatar,
|
||||||
text: strings.Premium_AvatarInfo,
|
text: strings.Premium_AvatarInfo,
|
||||||
|
@ -1161,13 +1161,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let termsString: MultilineTextComponent.TextContent
|
let termsString: MultilineTextComponent.TextContent
|
||||||
if context.component.isPremium == true {
|
// if context.component.isPremium == true {
|
||||||
if let promoConfiguration = context.state.promoConfiguration {
|
if let promoConfiguration = context.state.promoConfiguration {
|
||||||
let attributedString = stringWithAppliedEntities(promoConfiguration.status, entities: promoConfiguration.statusEntities, baseColor: termsTextColor, linkColor: environment.theme.list.itemAccentColor, baseFont: termsFont, linkFont: termsFont, boldFont: boldTermsFont, italicFont: italicTermsFont, boldItalicFont: boldItalicTermsFont, fixedFont: monospaceTermsFont, blockQuoteFont: termsFont)
|
let attributedString = stringWithAppliedEntities(promoConfiguration.status, entities: promoConfiguration.statusEntities, baseColor: termsTextColor, linkColor: environment.theme.list.itemAccentColor, baseFont: termsFont, linkFont: termsFont, boldFont: boldTermsFont, italicFont: italicTermsFont, boldItalicFont: boldItalicTermsFont, fixedFont: monospaceTermsFont, blockQuoteFont: termsFont)
|
||||||
termsString = .plain(attributedString)
|
termsString = .plain(attributedString)
|
||||||
} else {
|
|
||||||
termsString = .plain(NSAttributedString())
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
termsString = .markdown(
|
termsString = .markdown(
|
||||||
text: strings.Premium_Terms,
|
text: strings.Premium_Terms,
|
||||||
@ -1403,7 +1400,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
|> deliverOnMainQueue).start(next: { [weak self] available in
|
|> deliverOnMainQueue).start(next: { [weak self] available in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if available {
|
if available {
|
||||||
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct, account: strongSelf.context.account)
|
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
if let strongSelf = self, case .purchased = status {
|
if let strongSelf = self, case .purchased = status {
|
||||||
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)
|
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)
|
||||||
@ -1418,8 +1415,14 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
|> mapToSignal { _ -> Signal<Never, AssignAppStoreTransactionError> in
|
|> mapToSignal { _ -> Signal<Never, AssignAppStoreTransactionError> in
|
||||||
return .never()
|
return .never()
|
||||||
}
|
}
|
||||||
|
|> timeout(15.0, queue: Queue.mainQueue(), alternate: .fail(.timeout))
|
||||||
|> deliverOnMainQueue).start(error: { _ in
|
|> deliverOnMainQueue).start(error: { _ in
|
||||||
|
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail")
|
||||||
|
|
||||||
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
|
||||||
|
let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
|
||||||
|
strongSelf.present(alertController)
|
||||||
}, completed: { [weak self] in
|
}, completed: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
|
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
|
||||||
@ -1444,6 +1447,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
|
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
|
||||||
case .notAllowed:
|
case .notAllowed:
|
||||||
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
|
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
|
||||||
|
case .cantMakePayments:
|
||||||
|
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
|
||||||
|
case .assignFailed:
|
||||||
|
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
204
submodules/PremiumUI/Sources/SwirlStarsView.swift
Normal file
204
submodules/PremiumUI/Sources/SwirlStarsView.swift
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import SceneKit
|
||||||
|
import Display
|
||||||
|
import AppBundle
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
|
final class SwirlStarsView: UIView, PhoneDemoDecorationView {
|
||||||
|
private let sceneView: SCNView
|
||||||
|
|
||||||
|
private var particles: SCNNode?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
|
||||||
|
self.sceneView.backgroundColor = .clear
|
||||||
|
if let url = getAppBundle().url(forResource: "swirl", withExtension: "scn") {
|
||||||
|
self.sceneView.scene = try? SCNScene(url: url, options: nil)
|
||||||
|
}
|
||||||
|
self.sceneView.isUserInteractionEnabled = false
|
||||||
|
self.sceneView.preferredFramesPerSecond = 60
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.alpha = 0.0
|
||||||
|
|
||||||
|
self.addSubview(self.sceneView)
|
||||||
|
|
||||||
|
self.particles = self.sceneView.scene?.rootNode.childNode(withName: "particles", recursively: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.particles = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVisible(_ visible: Bool) {
|
||||||
|
if visible, let particles = self.particles, particles.parent == nil {
|
||||||
|
self.sceneView.scene?.rootNode.addChildNode(particles)
|
||||||
|
}
|
||||||
|
self.setupAnimations()
|
||||||
|
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
||||||
|
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.6 : 0.0, completion: { [weak self] finished in
|
||||||
|
if let strongSelf = self, finished && !visible && strongSelf.particles?.parent != nil {
|
||||||
|
strongSelf.particles?.removeFromParentNode()
|
||||||
|
|
||||||
|
if let node = strongSelf.sceneView.scene?.rootNode.childNode(withName: "star", recursively: false) {
|
||||||
|
node.removeAllAnimations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupAnimations() {
|
||||||
|
guard let node = self.sceneView.scene?.rootNode.childNode(withName: "star", recursively: false), node.animationKeys.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let initial = node.eulerAngles
|
||||||
|
let target = SCNVector3(x: node.eulerAngles.x + .pi * 2.0, y: node.eulerAngles.y, z: node.eulerAngles.z)
|
||||||
|
|
||||||
|
let animation = CABasicAnimation(keyPath: "eulerAngles")
|
||||||
|
animation.fromValue = NSValue(scnVector3: initial)
|
||||||
|
animation.toValue = NSValue(scnVector3: target)
|
||||||
|
animation.duration = 1.5
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||||
|
animation.fillMode = .forwards
|
||||||
|
animation.repeatCount = .infinity
|
||||||
|
node.addAnimation(animation, forKey: "rotation")
|
||||||
|
|
||||||
|
self.setupMovementAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupMovementAnimation() {
|
||||||
|
guard let node = self.sceneView.scene?.rootNode.childNode(withName: "star", recursively: false) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node.position = SCNVector3(3.5, 0.0, -2.0)
|
||||||
|
let firstPath = UIBezierPath()
|
||||||
|
firstPath.move(to: CGPoint(x: 3.5, y: -2.0))
|
||||||
|
firstPath.addLine(to: CGPoint(x: -15.5, y: 15.5))
|
||||||
|
|
||||||
|
let firstAction = SCNAction.moveAlong(path: firstPath, duration: 2.0)
|
||||||
|
|
||||||
|
SCNTransaction.begin()
|
||||||
|
SCNTransaction.animationDuration = 2.0
|
||||||
|
node.runAction(firstAction)
|
||||||
|
SCNTransaction.completionBlock = { [weak self, weak node] in
|
||||||
|
Queue.mainQueue().after(2.2, {
|
||||||
|
node?.position = SCNVector3(0.0, 0.0, -3.0)
|
||||||
|
let secondPath = UIBezierPath()
|
||||||
|
secondPath.move(to: CGPoint(x: 0.0, y: -3.0))
|
||||||
|
secondPath.addLine(to: CGPoint(x: 15.5, y: 20.0))
|
||||||
|
|
||||||
|
let secondAction = SCNAction.moveAlong(path: secondPath, duration: 2.0)
|
||||||
|
SCNTransaction.begin()
|
||||||
|
SCNTransaction.animationDuration = 2.0
|
||||||
|
node?.runAction(secondAction)
|
||||||
|
SCNTransaction.completionBlock = { [weak self] in
|
||||||
|
Queue.mainQueue().after(2.2, {
|
||||||
|
self?.setupMovementAnimation()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SCNTransaction.commit()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SCNTransaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetAnimation() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIBezierPath {
|
||||||
|
var elements: [PathElement] {
|
||||||
|
var pathElements = [PathElement]()
|
||||||
|
withUnsafeMutablePointer(to: &pathElements) { elementsPointer in
|
||||||
|
cgPath.apply(info: elementsPointer) { (userInfo, nextElementPointer) in
|
||||||
|
let nextElement = PathElement(element: nextElementPointer.pointee)
|
||||||
|
let elementsPointer = userInfo!.assumingMemoryBound(to: [PathElement].self)
|
||||||
|
elementsPointer.pointee.append(nextElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pathElements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PathElement {
|
||||||
|
case moveToPoint(CGPoint)
|
||||||
|
case addLineToPoint(CGPoint)
|
||||||
|
case addQuadCurveToPoint(CGPoint, CGPoint)
|
||||||
|
case addCurveToPoint(CGPoint, CGPoint, CGPoint)
|
||||||
|
case closeSubpath
|
||||||
|
|
||||||
|
init(element: CGPathElement) {
|
||||||
|
switch element.type {
|
||||||
|
case .moveToPoint:
|
||||||
|
self = .moveToPoint(element.points[0])
|
||||||
|
case .addLineToPoint:
|
||||||
|
self = .addLineToPoint(element.points[0])
|
||||||
|
case .addQuadCurveToPoint:
|
||||||
|
self = .addQuadCurveToPoint(element.points[0], element.points[1])
|
||||||
|
case .addCurveToPoint:
|
||||||
|
self = .addCurveToPoint(element.points[0], element.points[1], element.points[2])
|
||||||
|
case .closeSubpath:
|
||||||
|
self = .closeSubpath
|
||||||
|
@unknown default:
|
||||||
|
self = .closeSubpath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension SCNAction {
|
||||||
|
class func moveAlong(path: UIBezierPath, duration animationDuration: Double) -> SCNAction {
|
||||||
|
let points = path.elements
|
||||||
|
var actions = [SCNAction]()
|
||||||
|
|
||||||
|
for point in points {
|
||||||
|
switch point {
|
||||||
|
case .moveToPoint(let a):
|
||||||
|
let moveAction = SCNAction.move(to: SCNVector3(a.x, 0, a.y), duration: animationDuration)
|
||||||
|
actions.append(moveAction)
|
||||||
|
break
|
||||||
|
|
||||||
|
case .addCurveToPoint(let a, let b, let c):
|
||||||
|
let moveAction1 = SCNAction.move(to: SCNVector3(a.x, 0, a.y), duration: animationDuration)
|
||||||
|
let moveAction2 = SCNAction.move(to: SCNVector3(b.x, 0, b.y), duration: animationDuration)
|
||||||
|
let moveAction3 = SCNAction.move(to: SCNVector3(c.x, 0, c.y), duration: animationDuration)
|
||||||
|
actions.append(moveAction1)
|
||||||
|
actions.append(moveAction2)
|
||||||
|
actions.append(moveAction3)
|
||||||
|
break
|
||||||
|
|
||||||
|
case .addLineToPoint(let a):
|
||||||
|
let moveAction = SCNAction.move(to: SCNVector3(a.x, 0, a.y), duration: animationDuration)
|
||||||
|
actions.append(moveAction)
|
||||||
|
break
|
||||||
|
|
||||||
|
case .addQuadCurveToPoint(let a, let b):
|
||||||
|
let moveAction1 = SCNAction.move(to: SCNVector3(a.x, 0, a.y), duration: animationDuration)
|
||||||
|
let moveAction2 = SCNAction.move(to: SCNVector3(b.x, 0, b.y), duration: animationDuration)
|
||||||
|
actions.append(moveAction1)
|
||||||
|
actions.append(moveAction2)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
let moveAction = SCNAction.move(to: SCNVector3(0, 0, 0), duration: animationDuration)
|
||||||
|
actions.append(moveAction)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SCNAction.sequence(actions)
|
||||||
|
}
|
||||||
|
}
|
@ -321,46 +321,82 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generatePremiumReactionIcon() -> UIImage? {
|
private let starsCount = 7
|
||||||
return generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in
|
private final class StarsNode: ASDisplayNode {
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
private let starNodes: [ASImageNode]
|
||||||
if let backgroundImage = UIImage(bundleImageName: "Premium/BackgroundIcon"), let foregroundImage = UIImage(bundleImageName: "Premium/ForegroundIcon") {
|
private var timer: SwiftSignalKit.Timer?
|
||||||
context.saveGState()
|
|
||||||
if let cgImage = backgroundImage.cgImage {
|
override init() {
|
||||||
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
|
let image = UIImage(bundleImageName: "Premium/ReactionsStar")
|
||||||
}
|
var starNodes: [ASImageNode] = []
|
||||||
|
for _ in 0 ..< starsCount {
|
||||||
let colorsArray: [CGColor] = [
|
let node = ASImageNode()
|
||||||
UIColor(rgb: 0x6B93FF).cgColor,
|
node.alpha = 0.0
|
||||||
UIColor(rgb: 0x6B93FF).cgColor,
|
node.image = image
|
||||||
UIColor(rgb: 0x976FFF).cgColor,
|
node.displaysAsynchronously = false
|
||||||
UIColor(rgb: 0xE46ACE).cgColor,
|
starNodes.append(node)
|
||||||
UIColor(rgb: 0xE46ACE).cgColor
|
|
||||||
]
|
|
||||||
var locations: [CGFloat] = [0.0, 0.15, 0.5, 0.85, 1.0]
|
|
||||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
|
|
||||||
|
|
||||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
|
|
||||||
|
|
||||||
context.restoreGState()
|
|
||||||
|
|
||||||
if let cgImage = foregroundImage.cgImage {
|
|
||||||
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
|
|
||||||
}
|
|
||||||
context.setFillColor(UIColor.white.cgColor)
|
|
||||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
|
||||||
}
|
}
|
||||||
})
|
self.starNodes = starNodes
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
for node in starNodes {
|
||||||
|
self.addSubnode(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setup(firstTime: true)
|
||||||
|
|
||||||
|
self.timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
|
||||||
|
self?.setup()
|
||||||
|
}, queue: Queue.mainQueue())
|
||||||
|
self.timer?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.timer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(firstTime: Bool = false) {
|
||||||
|
let size = CGSize(width: 32.0, height: 32.0)
|
||||||
|
let starSize = CGSize(width: 6.0, height: 8.0)
|
||||||
|
|
||||||
|
for node in self.starNodes {
|
||||||
|
if node.layer.animation(forKey: "transform.scale") == nil && node.layer.animation(forKey: "opacity") == nil {
|
||||||
|
let x = CGFloat.random(in: 0 ..< size.width)
|
||||||
|
let y = CGFloat.random(in: 0 ..< size.width)
|
||||||
|
|
||||||
|
let randomTargetScale = CGFloat.random(in: 0.8 ..< 1.0)
|
||||||
|
node.bounds = CGRect(origin: .zero, size: starSize)
|
||||||
|
node.position = CGPoint(x: x, y: y)
|
||||||
|
|
||||||
|
node.alpha = 1.0
|
||||||
|
|
||||||
|
let duration = CGFloat.random(in: 0.4 ..< 0.65)
|
||||||
|
let delay = firstTime ? CGFloat.random(in: 0.0 ..< 0.25) : 0.0
|
||||||
|
node.layer.animateScale(from: 0.001, to: randomTargetScale, duration: duration, delay: delay, removeOnCompletion: false, completion: { [weak self, weak node] _ in
|
||||||
|
let duration = CGFloat.random(in: 0.3 ..< 0.35)
|
||||||
|
node?.alpha = 0.0
|
||||||
|
node?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak node] _ in
|
||||||
|
node?.layer.removeAllAnimations()
|
||||||
|
self?.setup()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
||||||
var isExtracted: Bool = false
|
var isExtracted: Bool = false
|
||||||
|
|
||||||
var backgroundView: UIVisualEffectView?
|
private var backgroundView: UIVisualEffectView?
|
||||||
let backgroundMaskNode: ASImageNode
|
private let backgroundMaskNode: ASImageNode
|
||||||
let backgroundOverlayNode: ASImageNode
|
private let backgroundOverlayNode: ASImageNode
|
||||||
let imageNode: ASImageNode
|
private let imageNode: ASImageNode
|
||||||
let maskImageNode: ASImageNode
|
private var starsNode: StarsNode?
|
||||||
|
|
||||||
|
private let maskContainerNode: ASDisplayNode
|
||||||
|
private let maskImageNode: ASImageNode
|
||||||
|
|
||||||
init(theme: PresentationTheme) {
|
init(theme: PresentationTheme) {
|
||||||
self.backgroundMaskNode = ASImageNode()
|
self.backgroundMaskNode = ASImageNode()
|
||||||
@ -370,7 +406,7 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
|||||||
self.backgroundMaskNode.image = UIImage(bundleImageName: "Premium/ReactionsBackground")
|
self.backgroundMaskNode.image = UIImage(bundleImageName: "Premium/ReactionsBackground")
|
||||||
|
|
||||||
self.backgroundOverlayNode = ASImageNode()
|
self.backgroundOverlayNode = ASImageNode()
|
||||||
self.backgroundOverlayNode.alpha = 0.05
|
self.backgroundOverlayNode.alpha = 0.1
|
||||||
self.backgroundOverlayNode.contentMode = .center
|
self.backgroundOverlayNode.contentMode = .center
|
||||||
self.backgroundOverlayNode.displaysAsynchronously = false
|
self.backgroundOverlayNode.displaysAsynchronously = false
|
||||||
self.backgroundOverlayNode.isUserInteractionEnabled = false
|
self.backgroundOverlayNode.isUserInteractionEnabled = false
|
||||||
@ -382,20 +418,24 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
|||||||
self.imageNode.isUserInteractionEnabled = false
|
self.imageNode.isUserInteractionEnabled = false
|
||||||
self.imageNode.image = UIImage(bundleImageName: "Premium/ReactionsForeground")
|
self.imageNode.image = UIImage(bundleImageName: "Premium/ReactionsForeground")
|
||||||
|
|
||||||
|
self.maskContainerNode = ASDisplayNode()
|
||||||
|
|
||||||
self.maskImageNode = ASImageNode()
|
self.maskImageNode = ASImageNode()
|
||||||
if let backgroundImage = UIImage(bundleImageName: "Premium/ReactionsBackground") {
|
if let backgroundImage = UIImage(bundleImageName: "Premium/ReactionsBackground") {
|
||||||
self.maskImageNode.image = generateImage(CGSize(width: 40.0, height: 52.0), contextGenerator: { size, context in
|
self.maskImageNode.image = generateImage(CGSize(width: 40.0 * 4.0, height: 52.0 * 4.0), contextGenerator: { size, context in
|
||||||
context.setFillColor(UIColor.black.cgColor)
|
context.setFillColor(UIColor.black.cgColor)
|
||||||
context.fill(CGRect(origin: .zero, size: size))
|
context.fill(CGRect(origin: .zero, size: size))
|
||||||
|
|
||||||
if let cgImage = backgroundImage.cgImage {
|
if let cgImage = backgroundImage.cgImage {
|
||||||
let maskFrame = CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 10.0)
|
let maskFrame = CGRect(origin: .zero, size: size).insetBy(dx: 4.0 + 40.0 * 2.0 - 16.0, dy: 10.0 + 52.0 * 2.0 - 16.0)
|
||||||
context.clip(to: maskFrame, mask: cgImage)
|
context.clip(to: maskFrame, mask: cgImage)
|
||||||
}
|
}
|
||||||
context.setBlendMode(.clear)
|
context.setBlendMode(.clear)
|
||||||
context.fill(CGRect(origin: .zero, size: size))
|
context.fill(CGRect(origin: .zero, size: size))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
self.maskImageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((40.0 - 40.0 * 4.0) / 2.0), y: floorToScreenPixels((52.0 - 52.0 * 4.0) / 2.0)), size: CGSize(width: 40.0 * 4.0, height: 52.0 * 4.0))
|
||||||
|
self.maskContainerNode.addSubnode(self.maskImageNode)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
@ -416,10 +456,37 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
|||||||
backgroundView.mask = self.backgroundMaskNode.view
|
backgroundView.mask = self.backgroundMaskNode.view
|
||||||
self.view.insertSubview(backgroundView, at: 0)
|
self.view.insertSubview(backgroundView, at: 0)
|
||||||
self.backgroundView = backgroundView
|
self.backgroundView = backgroundView
|
||||||
|
|
||||||
|
let starsNode = StarsNode()
|
||||||
|
starsNode.frame = CGRect(origin: .zero, size: CGSize(width: 32.0, height: 32.0))
|
||||||
|
self.backgroundView?.contentView.addSubview(starsNode.view)
|
||||||
|
self.starsNode = starsNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func appear(animated: Bool) {
|
func appear(animated: Bool) {
|
||||||
|
if animated {
|
||||||
|
let delay: Double = 0.1
|
||||||
|
let duration: Double = 0.85
|
||||||
|
let damping: CGFloat = 60.0
|
||||||
|
|
||||||
|
let initialScale: CGFloat = 0.25
|
||||||
|
self.maskImageNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: delay, damping: damping)
|
||||||
|
self.backgroundView?.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: delay, damping: damping)
|
||||||
|
self.backgroundOverlayNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: delay, damping: damping)
|
||||||
|
self.imageNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: delay, damping: damping)
|
||||||
|
|
||||||
|
Queue.mainQueue().after(0.25, {
|
||||||
|
let shimmerNode = ASImageNode()
|
||||||
|
shimmerNode.displaysAsynchronously = false
|
||||||
|
shimmerNode.image = generateGradientImage(size: CGSize(width: 32.0, height: 32.0), colors: [UIColor(rgb: 0xffffff, alpha: 0.0), UIColor(rgb: 0xffffff, alpha: 0.24), UIColor(rgb: 0xffffff, alpha: 0.0)], locations: [0.0, 0.5, 1.0], direction: .horizontal)
|
||||||
|
shimmerNode.frame = CGRect(origin: .zero, size: CGSize(width: 32.0, height: 32.0))
|
||||||
|
self.backgroundView?.contentView.addSubview(shimmerNode.view)
|
||||||
|
|
||||||
|
shimmerNode.layer.animatePosition(from: CGPoint(x: -60.0, y: 0.0), to: CGPoint(x: 60.0, y: 0.0), duration: 0.75, removeOnCompletion: false, additive: true, completion: { [weak shimmerNode] _ in
|
||||||
|
shimmerNode?.view.removeFromSuperview()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) {
|
func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
@ -430,8 +497,7 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode {
|
|||||||
self.imageNode.frame = bounds
|
self.imageNode.frame = bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var maskNode: ASDisplayNode? {
|
var maskNode: ASDisplayNode? {
|
||||||
return self.maskImageNode
|
return self.maskContainerNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import TelegramApi
|
|||||||
|
|
||||||
public enum AssignAppStoreTransactionError {
|
public enum AssignAppStoreTransactionError {
|
||||||
case generic
|
case generic
|
||||||
|
case timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
func _internal_sendAppStoreReceipt(account: Account, receipt: Data, restore: Bool) -> Signal<Never, AssignAppStoreTransactionError> {
|
func _internal_sendAppStoreReceipt(account: Account, receipt: Data, restore: Bool) -> Signal<Never, AssignAppStoreTransactionError> {
|
||||||
|
21
submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ministar@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/ministar@3x.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/ReactionsStar.imageset/ministar@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 341 B |
Loading…
x
Reference in New Issue
Block a user