Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,172 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
import SwiftSignalKit
private final class RadialCheckContentNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
super.init()
}
}
final class RadialCheckContentNode: RadialStatusContentNode {
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 1.0 {
didSet {
self.setNeedsDisplay()
}
}
private var animationCompletionTimer: SwiftSignalKit.Timer?
private var isAnimatingProgress: Bool {
return self.pop_animation(forKey: "progress") != nil || self.animationCompletionTimer != nil
}
private var enqueuedReadyForTransition: (() -> Void)?
init(color: UIColor) {
self.color = color
super.init()
self.displaysAsynchronously = true
self.isOpaque = false
self.isLayerBacked = true
}
func animateProgress(delay: Double) {
self.animationCompletionTimer?.invalidate()
self.animationCompletionTimer = nil
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialCheckContentNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialCheckContentNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = 0.0 as NSNumber
animation.toValue = 1.0 as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.25
animation.beginTime = delay
animation.completionBlock = { [weak self] _, _ in
if let strongSelf = self {
strongSelf.animationCompletionTimer?.invalidate()
if let strongSelf = self {
strongSelf.animationCompletionTimer = nil
if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition {
strongSelf.enqueuedReadyForTransition = nil
enqueuedReadyForTransition()
}
}
}
}
self.pop_add(animation, forKey: "progress")
}
override func enqueueReadyForTransition(_ f: @escaping () -> Void) {
if self.isAnimatingProgress {
self.enqueuedReadyForTransition = f
} else {
f()
}
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialCheckContentNodeParameters(color: self.color, progress: self.effectiveProgress)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialCheckContentNodeParameters {
let diameter = bounds.size.width
let progress = parameters.progress
var pathLineWidth: CGFloat = 2.0
if (abs(diameter - 37.0) < 0.1) {
pathLineWidth = 2.5
} else if (abs(diameter - 32.0) < 0.1) {
pathLineWidth = 2.0
} else {
pathLineWidth = 2.5
}
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
let factor: CGFloat = max(0.3, diameter / 50.0)
context.setStrokeColor(parameters.color.cgColor)
context.setLineWidth(max(1.7, pathLineWidth * factor))
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor)
var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor)
var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor)
if diameter < 36.0 {
s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor)
p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor)
p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor)
}
if !firstSegment.isZero {
if firstSegment < 1.0 {
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
context.addLine(to: s)
} else {
let secondSegment = (progress - 0.33) * 1.5
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
context.addLine(to: s)
}
}
context.strokePath()
}
}
private let duration: Double = 0.2
override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { _ in
completion()
})
self.layer.animateScale(from: 1.0, to: 0.6, duration: duration, removeOnCompletion: false)
}
override func animateIn(from: RadialStatusNodeState, delay: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, delay: delay)
self.layer.animateScale(from: 0.7, to: 1.0, duration: duration, delay: delay)
self.animateProgress(delay: delay)
}
}
@@ -0,0 +1,302 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private final class RadialCloudProgressContentCancelNodeParameters: NSObject {
let color: UIColor
init(color: UIColor) {
self.color = color
}
}
private final class RadialCloudProgressContentSpinnerNodeParameters: NSObject {
let color: UIColor
let backgroundStrokeColor: UIColor
let progress: CGFloat
let lineWidth: CGFloat?
init(color: UIColor, backgroundStrokeColor: UIColor, progress: CGFloat, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.progress = progress
self.lineWidth = lineWidth
}
}
private final class RadialCloudProgressContentSpinnerNode: ASDisplayNode {
var progressAnimationCompleted: (() -> Void)?
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
var backgroundStrokeColor: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
var progress: CGFloat? {
didSet {
self.pop_removeAnimation(forKey: "progress")
if let progress = self.progress {
self.pop_removeAnimation(forKey: "indefiniteProgress")
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = CGFloat(self.effectiveProgress) as NSNumber
animation.toValue = CGFloat(progress) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.2
animation.completionBlock = { [weak self] _, _ in
self?.progressAnimationCompleted?()
}
self.pop_add(animation, forKey: "progress")
} else if self.pop_animation(forKey: "indefiniteProgress") == nil {
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = CGFloat(0.0) as NSNumber
animation.toValue = CGFloat(2.0) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 2.5
animation.repeatForever = true
self.pop_add(animation, forKey: "indefiniteProgress")
}
}
}
var isAnimatingProgress: Bool {
return self.pop_animation(forKey: "progress") != nil
}
let lineWidth: CGFloat?
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.lineWidth = lineWidth
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialCloudProgressContentSpinnerNodeParameters(color: self.color, backgroundStrokeColor: self.backgroundStrokeColor, progress: self.effectiveProgress, lineWidth: self.lineWidth)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialCloudProgressContentSpinnerNodeParameters {
let factor = bounds.size.width / 50.0
var progress = parameters.progress
var startAngle = -CGFloat.pi / 2.0
var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
let lineWidth: CGFloat = parameters.lineWidth ?? max(1.6, 2.25 * factor)
let pathDiameter: CGFloat
if parameters.lineWidth != nil {
pathDiameter = bounds.size.width - lineWidth
} else {
pathDiameter = bounds.size.width - lineWidth - 2.5 * 2.0
}
context.setStrokeColor(parameters.backgroundStrokeColor.cgColor)
let backgroundPath = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: 0.0, endAngle: 2.0 * CGFloat.pi, clockwise:true)
backgroundPath.lineWidth = lineWidth
backgroundPath.stroke()
context.setStrokeColor(parameters.color.cgColor)
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
}
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
basicAnimation.duration = 2.0
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
basicAnimation.beginTime = 1.0
self.layer.add(basicAnimation, forKey: "progressRotation")
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.layer.removeAnimation(forKey: "progressRotation")
}
}
private final class RadialCloudProgressContentCancelNode: ASDisplayNode {
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
init(color: UIColor) {
self.color = color
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialCloudProgressContentCancelNodeParameters(color: self.color)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialCloudProgressContentCancelNodeParameters {
let size: CGFloat = 8.0
context.setFillColor(parameters.color.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: floor((bounds.size.width - size) / 2.0), y: floor((bounds.size.height - size) / 2.0)), size: CGSize(width: size, height: size)), cornerRadius: 2.0)
path.fill()
}
}
}
final class RadialCloudProgressContentNode: RadialStatusContentNode {
private let spinnerNode: RadialCloudProgressContentSpinnerNode
private let cancelNode: RadialCloudProgressContentCancelNode
var color: UIColor {
didSet {
self.setNeedsDisplay()
self.spinnerNode.color = self.color
}
}
var backgroundStrokeColor: UIColor {
didSet {
self.setNeedsDisplay()
self.spinnerNode.backgroundStrokeColor = self.backgroundStrokeColor
}
}
var progress: CGFloat? = 0.0 {
didSet {
self.spinnerNode.progress = self.progress
}
}
private var enqueuedReadyForTransition: (() -> Void)?
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.spinnerNode = RadialCloudProgressContentSpinnerNode(color: color, backgroundStrokeColor: backgroundStrokeColor, lineWidth: lineWidth)
self.cancelNode = RadialCloudProgressContentCancelNode(color: color)
super.init()
self.isLayerBacked = true
self.addSubnode(self.spinnerNode)
self.addSubnode(self.cancelNode)
self.spinnerNode.progressAnimationCompleted = { [weak self] in
if let strongSelf = self {
if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition {
strongSelf.enqueuedReadyForTransition = nil
enqueuedReadyForTransition()
}
}
}
}
override func enqueueReadyForTransition(_ f: @escaping () -> Void) {
if self.spinnerNode.isAnimatingProgress && self.progress == 1.0 {
self.enqueuedReadyForTransition = f
} else {
f()
}
}
override func layout() {
super.layout()
let bounds = self.bounds
self.spinnerNode.bounds = bounds
self.spinnerNode.position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
self.cancelNode.frame = bounds
}
override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
completion()
})
self.cancelNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false)
}
override func animateIn(from: RadialStatusNodeState, delay: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
self.cancelNode.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: delay)
}
}
@@ -0,0 +1,224 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
import SwiftSignalKit
private extension CAShapeLayer {
func animateStrokeStart(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "strokeStart", timingFunction: timingFunction, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: completion)
}
func animateStrokeEnd(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "strokeEnd", timingFunction: timingFunction, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: completion)
}
}
final class RadialDownloadContentNode: RadialStatusContentNode {
var color: UIColor {
didSet {
self.leftLine.strokeColor = self.color.cgColor
self.rightLine.strokeColor = self.color.cgColor
self.arrowBody.strokeColor = self.color.cgColor
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 1.0 {
didSet {
self.setNeedsDisplay()
}
}
private var enqueuedReadyForTransition: (() -> Void)?
private var isAnimatingTransition = false
private let leftLine = CAShapeLayer()
private let rightLine = CAShapeLayer()
private let arrowBody = CAShapeLayer()
init(color: UIColor) {
self.color = color
super.init()
self.leftLine.fillColor = UIColor.clear.cgColor
self.leftLine.strokeColor = self.color.cgColor
self.leftLine.lineCap = .round
self.leftLine.lineJoin = .round
self.rightLine.fillColor = UIColor.clear.cgColor
self.rightLine.strokeColor = self.color.cgColor
self.rightLine.lineCap = .round
self.rightLine.lineJoin = .round
self.arrowBody.fillColor = UIColor.clear.cgColor
self.arrowBody.strokeColor = self.color.cgColor
self.arrowBody.lineCap = .round
self.arrowBody.lineJoin = .round
self.isLayerBacked = true
self.isOpaque = false
self.layer.addSublayer(self.arrowBody)
self.layer.addSublayer(self.leftLine)
self.layer.addSublayer(self.rightLine)
}
override func enqueueReadyForTransition(_ f: @escaping () -> Void) {
if self.isAnimatingTransition {
self.enqueuedReadyForTransition = f
} else {
f()
}
}
private func svgPath(_ path: StaticString, scale: CGPoint = CGPoint(x: 1.0, y: 1.0), offset: CGPoint = CGPoint()) throws -> UIBezierPath {
var index: UnsafePointer<UInt8> = path.utf8Start
let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount)
let path = UIBezierPath()
while index < end {
let c = index.pointee
index = index.successor()
if c == 77 { // M
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
path.move(to: CGPoint(x: x, y: y))
} else if c == 76 { // L
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
path.addLine(to: CGPoint(x: x, y: y))
} else if c == 67 { // C
let x1 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y1 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
let x2 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y2 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
path.addCurve(to: CGPoint(x: x, y: y), controlPoint1: CGPoint(x: x1, y: y1), controlPoint2: CGPoint(x: x2, y: y2))
} else if c == 32 { // space
continue
}
}
return path
}
override func layout() {
super.layout()
let bounds = self.bounds
let diameter = min(bounds.size.width, bounds.size.height)
let factor = diameter / 50.0
let lineWidth: CGFloat = max(1.6, 2.25 * factor)
self.leftLine.lineWidth = lineWidth
self.rightLine.lineWidth = lineWidth
self.arrowBody.lineWidth = lineWidth
let arrowHeadSize: CGFloat = 15.0 * factor
let arrowLength: CGFloat = 18.0 * factor
let arrowHeadOffset: CGFloat = 1.0 * factor
let leftPath = UIBezierPath()
leftPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
leftPath.addLine(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
self.leftLine.path = leftPath.cgPath
let rightPath = UIBezierPath()
rightPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
rightPath.addLine(to: CGPoint(x: diameter / 2.0 + arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
self.rightLine.path = rightPath.cgPath
if self.delayPrepareAnimateIn {
self.delayPrepareAnimateIn = false
self.prepareAnimateIn(from: nil)
}
}
private let duration: Double = 0.2
override func prepareAnimateOut(completion: @escaping (Double) -> Void) {
let bounds = self.bounds
let diameter = min(bounds.size.width, bounds.size.height)
let factor = diameter / 50.0
var bodyPath = UIBezierPath()
if let path = try? svgPath("M1.10890748,47.3077093 C2.74202161,51.7201715 4.79761832,55.7299828 7.15775768,59.3122505 C25.4413606,87.0634763 62.001605,89.1563513 62.0066002,54.0178571 L62.0066002,0.625 ", scale: CGPoint(x: 0.333333 * factor, y: 0.333333 * factor), offset: CGPoint(x: (4.0 + UIScreenPixel) * factor, y: (17.0 - UIScreenPixel) * factor)) {
bodyPath = path
}
self.arrowBody.path = bodyPath.cgPath
self.arrowBody.strokeStart = 0.65
self.leftLine.animateStrokeEnd(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.rightLine.animateStrokeEnd(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.leftLine.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.07, removeOnCompletion: false) { finished in
completion(0.0)
}
self.rightLine.animateAlpha(from: 1.0, to: 0.0, duration: 0.02, delay: 0.15, removeOnCompletion: false) { finished in
self.leftLine.strokeColor = UIColor.clear.cgColor
self.rightLine.strokeColor = UIColor.clear.cgColor
}
}
override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
if self.bounds.width < 21.0 {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { _ in
completion()
})
self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false)
} else {
self.isAnimatingTransition = true
self.arrowBody.animateStrokeStart(from: 0.65, to: 0.0, duration: 0.5, removeOnCompletion: false, completion: { [weak self] _ in
completion()
if let strongSelf = self, strongSelf.isAnimatingTransition, let f = strongSelf.enqueuedReadyForTransition {
strongSelf.isAnimatingTransition = false
f()
}
})
self.arrowBody.animateStrokeEnd(from: 1.0, to: 0.0, duration: 0.5, removeOnCompletion: false, completion: nil)
self.arrowBody.animateAlpha(from: 1.0, to: 0.0, duration: 0.01, delay: 0.4, removeOnCompletion: false)
}
}
private var delayPrepareAnimateIn = false
override func prepareAnimateIn(from: RadialStatusNodeState?) {
let bounds = self.bounds
let diameter = min(bounds.size.width, bounds.size.height)
guard !diameter.isZero else {
self.delayPrepareAnimateIn = true
return
}
let factor = diameter / 50.0
var bodyPath = UIBezierPath()
if let path = try? svgPath("M1.10890748,47.3077093 C2.74202161,51.7201715 4.79761832,55.7299828 7.15775768,59.3122505 C25.4413606,87.0634763 62.001605,89.1563513 62.0066002,54.0178571 L62.0066002,0.625 ", scale: CGPoint(x: -0.333333 * factor, y: 0.333333 * factor), offset: CGPoint(x: (46.0 - UIScreenPixel) * factor, y: (17.0 - UIScreenPixel) * factor)) {
bodyPath = path
}
self.arrowBody.path = bodyPath.cgPath
self.arrowBody.strokeStart = 0.65
}
override func animateIn(from: RadialStatusNodeState, delay: Double) {
if case .progress = from {
self.arrowBody.animateStrokeStart(from: 0.65, to: 0.65, duration: 0.25, delay: delay, removeOnCompletion: false, completion: nil)
self.arrowBody.animateStrokeEnd(from: 0.65, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false, completion: nil)
self.leftLine.animateStrokeEnd(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
self.rightLine.animateStrokeEnd(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
self.arrowBody.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
self.leftLine.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
self.rightLine.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
} else {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, delay: delay)
self.layer.animateScale(from: 0.7, to: 1.0, duration: duration, delay: delay)
}
}
}
@@ -0,0 +1,343 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
import SwiftSignalKit
private final class RadialProgressContentCancelNodeParameters: NSObject {
let color: UIColor
let displayCancel: Bool
init(color: UIColor, displayCancel: Bool) {
self.color = color
self.displayCancel = displayCancel
}
}
private final class RadialProgressContentSpinnerNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
let lineWidth: CGFloat?
init(color: UIColor, progress: CGFloat, lineWidth: CGFloat?) {
self.color = color
self.progress = progress
self.lineWidth = lineWidth
}
}
private final class RadialProgressContentSpinnerNode: ASDisplayNode {
var progressAnimationCompleted: (() -> Void)?
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
var progress: CGFloat? {
didSet {
self.pop_removeAnimation(forKey: "progress")
if let progress = self.progress {
self.pop_removeAnimation(forKey: "indefiniteProgress")
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
var duration = 0.2
let delta = max(0.0, progress - self.effectiveProgress)
if delta > 0.25 {
duration += Double(min(0.45, 0.45 * ((delta - 0.25) * 5)))
}
animation.fromValue = CGFloat(self.effectiveProgress) as NSNumber
animation.toValue = CGFloat(progress) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = duration
animation.completionBlock = { [weak self] _, _ in
self?.progressAnimationCompleted?()
}
self.pop_add(animation, forKey: "progress")
} else if self.pop_animation(forKey: "indefiniteProgress") == nil {
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = CGFloat(0.0) as NSNumber
animation.toValue = CGFloat(2.0) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 2.5
animation.repeatForever = true
self.pop_add(animation, forKey: "indefiniteProgress")
}
}
}
var isAnimatingProgress: Bool {
return self.pop_animation(forKey: "progress") != nil
}
let lineWidth: CGFloat?
private let animateRotation: Bool
init(color: UIColor, lineWidth: CGFloat?, animateRotation: Bool) {
self.color = color
self.lineWidth = lineWidth
self.animateRotation = animateRotation
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialProgressContentSpinnerNodeParameters(color: self.color, progress: self.effectiveProgress, lineWidth: self.lineWidth)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialProgressContentSpinnerNodeParameters {
context.setStrokeColor(parameters.color.cgColor)
let factor = bounds.size.width / 50.0
var progress = parameters.progress
var startAngle = -CGFloat.pi / 2.0
var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
let lineWidth: CGFloat = parameters.lineWidth ?? max(1.6, 2.25 * factor)
let pathDiameter: CGFloat
if parameters.lineWidth != nil {
pathDiameter = bounds.size.width - lineWidth
} else {
pathDiameter = bounds.size.width - lineWidth - 2.5 * 2.0
}
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
}
}
private var hierarchyVersion: Int = 0
override func willEnterHierarchy() {
super.willEnterHierarchy()
if self.animateRotation {
self.hierarchyVersion += 1
if self.layer.animation(forKey: "progressRotation") == nil {
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.duration = 1.5
var fromValue = Float.pi + 0.58
if let presentation = self.layer.presentation(), let value = (presentation.value(forKeyPath: "transform.rotation.z") as? NSNumber)?.floatValue {
fromValue = value
}
basicAnimation.fromValue = NSNumber(value: fromValue)
basicAnimation.toValue = NSNumber(value: fromValue + Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
basicAnimation.beginTime = 0.0
self.layer.add(basicAnimation, forKey: "progressRotation")
}
}
}
override func didExitHierarchy() {
super.didExitHierarchy()
if self.animateRotation {
let version = self.hierarchyVersion
Queue.mainQueue().after(0.1, {
if self.hierarchyVersion == version {
self.layer.removeAnimation(forKey: "progressRotation")
}
})
}
}
}
private final class RadialProgressContentCancelNode: ASDisplayNode {
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
let displayCancel: Bool
init(color: UIColor, displayCancel: Bool) {
self.color = color
self.displayCancel = displayCancel
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialProgressContentCancelNodeParameters(color: self.color, displayCancel: self.displayCancel)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialProgressContentCancelNodeParameters {
if parameters.displayCancel {
let diameter = min(bounds.size.width, bounds.size.height)
let factor = diameter / 50.0
context.setStrokeColor(parameters.color.cgColor)
context.setLineWidth(max(1.3, 2.0 * factor))
context.setLineCap(.round)
let crossSize: CGFloat = 14.0 * factor
context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
context.strokePath()
context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
context.strokePath()
}
}
}
}
final class RadialProgressContentNode: RadialStatusContentNode {
private let spinnerNode: RadialProgressContentSpinnerNode
private let cancelNode: RadialProgressContentCancelNode
var color: UIColor {
didSet {
self.setNeedsDisplay()
self.spinnerNode.color = self.color
}
}
var progress: CGFloat? = 0.0 {
didSet {
if self.ready {
self.spinnerNode.progress = self.progress
}
}
}
let displayCancel: Bool
var ready: Bool = false
let animateRotation: Bool
private var enqueuedReadyForTransition: (() -> Void)?
init(color: UIColor, lineWidth: CGFloat?, displayCancel: Bool, animateRotation: Bool) {
self.color = color
self.displayCancel = displayCancel
self.animateRotation = animateRotation
self.spinnerNode = RadialProgressContentSpinnerNode(color: color, lineWidth: lineWidth, animateRotation: animateRotation)
self.cancelNode = RadialProgressContentCancelNode(color: color, displayCancel: displayCancel)
super.init()
self.isLayerBacked = true
self.addSubnode(self.spinnerNode)
self.addSubnode(self.cancelNode)
self.spinnerNode.progressAnimationCompleted = { [weak self] in
if let strongSelf = self {
if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition {
strongSelf.enqueuedReadyForTransition = nil
enqueuedReadyForTransition()
}
}
}
}
override func layout() {
super.layout()
let bounds = self.bounds
self.spinnerNode.bounds = bounds
self.spinnerNode.position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
self.cancelNode.frame = bounds
}
override func prepareAnimateOut(completion: @escaping (Double) -> Void) {
self.cancelNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.15, removeOnCompletion: false, completion: { _ in })
completion(0.0)
}
override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
completion()
})
}
override func prepareAnimateIn(from: RadialStatusNodeState?) {
self.ready = true
self.spinnerNode.progress = self.progress
}
override func animateIn(from: RadialStatusNodeState, delay: Double) {
if case .download = from {
} else {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay)
}
if case .none = from {
self.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2, delay: delay)
}
self.cancelNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2, delay: delay)
}
}
@@ -0,0 +1,31 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
class RadialStatusContentNode: ASDisplayNode {
func enqueueReadyForTransition(_ f: @escaping () -> Void) {
f()
}
private let duration: Double = 0.2
func prepareAnimateOut(completion: @escaping (Double) -> Void) {
completion(0.0)
}
func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { _ in
completion()
})
self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false)
}
func prepareAnimateIn(from: RadialStatusNodeState?) {
}
func animateIn(from: RadialStatusNodeState, delay: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, delay: delay)
self.layer.animateScale(from: 0.2, to: 1.0, duration: duration, delay: delay)
}
}
@@ -0,0 +1,120 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
enum RadialStatusIcon {
case custom(UIImage)
case timeout
case play(UIColor)
case pause(UIColor)
}
private final class RadialStatusIconContentNodeParameters: NSObject {
let icon: RadialStatusIcon
init(icon: RadialStatusIcon) {
self.icon = icon
super.init()
}
}
final class RadialStatusIconContentNode: RadialStatusContentNode {
let icon: RadialStatusIcon
private var animationNode: FireIconNode?
init(icon: RadialStatusIcon, synchronous: Bool) {
self.icon = icon
super.init()
self.displaysAsynchronously = !synchronous
// self.isLayerBacked = true
self.isOpaque = false
if case .timeout = icon {
let animationNode = FireIconNode(animate: true)
self.animationNode = animationNode
self.addSubnode(animationNode)
}
}
override func layout() {
super.layout()
var factor: CGFloat = 0.75
var offset: CGFloat = 0.0415
if self.bounds.width < 30.0 {
factor = 1.0
offset = 0.0
}
let size = floorToScreenPixels(self.bounds.width * factor)
self.animationNode?.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size)
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialStatusIconContentNodeParameters(icon: self.icon)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialStatusIconContentNodeParameters {
let diameter = min(bounds.size.width, bounds.size.height)
switch parameters.icon {
case .timeout:
break
case let .play(color):
context.setFillColor(color.cgColor)
let factor = diameter / 50.0
let size = CGSize(width: 15.0, height: 18.0)
context.translateBy(x: (diameter - size.width) / 2.0 + 1.5, y: (diameter - size.height) / 2.0)
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: factor, y: factor)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ")
context.fillPath()
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.translateBy(x: -(diameter - size.width) / 2.0 - 1.5, y: -(diameter - size.height) / 2.0)
case let .pause(color):
context.setFillColor(color.cgColor)
let factor = diameter / 50.0
let size = CGSize(width: 15.0, height: 16.0)
context.translateBy(x: (diameter - size.width) / 2.0, y: (diameter - size.height) / 2.0)
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: factor, y: factor)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ")
context.fillPath()
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0)
case let .custom(image):
image.draw(at: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0)))
}
}
}
}
@@ -0,0 +1,426 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
public enum RadialStatusNodeState: Equatable {
public enum SecretTimeoutIcon: Equatable {
case none
case image(UIImage)
case flame
public static func ==(lhs: SecretTimeoutIcon, rhs: SecretTimeoutIcon) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case let .image(lhsImage):
if case let .image(rhsImage) = rhs, lhsImage === rhsImage {
return true
} else {
return false
}
case .flame:
if case .flame = rhs {
return true
} else {
return false
}
}
}
}
case none
case download(UIColor)
case play(UIColor)
case pause(UIColor)
case progress(color: UIColor, lineWidth: CGFloat?, value: CGFloat?, cancelEnabled: Bool, animateRotation: Bool)
case cloudProgress(color: UIColor, strokeBackgroundColor: UIColor, lineWidth: CGFloat, value: CGFloat?)
case check(UIColor)
case customIcon(UIImage)
case staticTimeout
case secretTimeout(color: UIColor, icon: SecretTimeoutIcon, beginTime: Double, timeout: Double, sparks: Bool)
public static func ==(lhs: RadialStatusNodeState, rhs: RadialStatusNodeState) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case let .download(lhsColor):
if case let .download(rhsColor) = rhs, lhsColor.isEqual(rhsColor) {
return true
} else {
return false
}
case let .play(lhsColor):
if case let .play(rhsColor) = rhs, lhsColor.isEqual(rhsColor) {
return true
} else {
return false
}
case let .pause(lhsColor):
if case let .pause(rhsColor) = rhs, lhsColor.isEqual(rhsColor) {
return true
} else {
return false
}
case let .progress(lhsColor, lhsLineWidth, lhsValue, lhsCancelEnabled, lhsAnimateRotation):
if case let .progress(rhsColor, rhsLineWidth, rhsValue, rhsCancelEnabled, rhsAnimateRotation) = rhs, lhsColor.isEqual(rhsColor), lhsValue == rhsValue, lhsLineWidth == rhsLineWidth, lhsCancelEnabled == rhsCancelEnabled, lhsAnimateRotation == rhsAnimateRotation {
return true
} else {
return false
}
case let .cloudProgress(lhsColor, lhsStrokeBackgroundColor, lhsLineWidth, lhsValue):
if case let .cloudProgress(rhsColor, rhsStrokeBackgroundColor, rhsLineWidth, rhsValue) = rhs, lhsColor.isEqual(rhsColor), lhsStrokeBackgroundColor.isEqual(rhsStrokeBackgroundColor), lhsLineWidth.isEqual(to: rhsLineWidth), lhsValue == rhsValue {
return true
} else {
return false
}
case let .check(lhsColor):
if case let .check(rhsColor) = rhs, lhsColor.isEqual(rhsColor) {
return true
} else {
return false
}
case let .customIcon(lhsImage):
if case let .customIcon(rhsImage) = rhs, lhsImage === rhsImage {
return true
} else {
return false
}
case .staticTimeout:
if case .staticTimeout = rhs {
return true
} else {
return false
}
case let .secretTimeout(lhsColor, lhsIcon, lhsBeginTime, lhsTimeout, lhsSparks):
if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon == rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks {
return true
} else {
return false
}
}
}
func isPrimarilyEqual(to rhs: RadialStatusNodeState) -> Bool {
switch self {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case .download:
if case .download = rhs{
return true
} else {
return false
}
case .play:
if case .play = rhs {
return true
} else {
return false
}
case .pause:
if case .pause = rhs {
return true
} else {
return false
}
case let .progress(lhsColor, lhsLineWidth, lhsValue, lhsCancelEnabled, lhsAnimateRotation):
if case let .progress(rhsColor, rhsLineWidth, rhsValue, rhsCancelEnabled, rhsAnimateRotation) = rhs, lhsColor.isEqual(rhsColor), lhsValue == rhsValue, lhsLineWidth == rhsLineWidth, lhsCancelEnabled == rhsCancelEnabled, lhsAnimateRotation == rhsAnimateRotation {
return true
} else {
return false
}
case let .cloudProgress(lhsColor, lhsStrokeBackgroundColor, lhsLineWidth, lhsValue):
if case let .cloudProgress(rhsColor, rhsStrokeBackgroundColor, rhsLineWidth, rhsValue) = rhs, lhsColor.isEqual(rhsColor), lhsStrokeBackgroundColor.isEqual(rhsStrokeBackgroundColor), lhsLineWidth.isEqual(to: rhsLineWidth), lhsValue == rhsValue {
return true
} else {
return false
}
case .check:
if case .check = rhs {
return true
} else {
return false
}
case let .customIcon(lhsImage):
if case let .customIcon(rhsImage) = rhs, lhsImage === rhsImage {
return true
} else {
return false
}
case .staticTimeout:
if case .staticTimeout = rhs{
return true
} else {
return false
}
case let .secretTimeout(lhsColor, lhsIcon, lhsBeginTime, lhsTimeout, lhsSparks):
if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon == rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks {
return true
} else {
return false
}
}
}
func backgroundColor(color: UIColor) -> UIColor? {
switch self {
case .none:
return nil
default:
return color
}
}
func contentNode(current: RadialStatusContentNode?, synchronous: Bool) -> RadialStatusContentNode? {
switch self {
case .none:
return nil
case let .download(color):
return RadialDownloadContentNode(color: color)
case let .play(color):
return RadialStatusIconContentNode(icon: .play(color), synchronous: synchronous)
case let .pause(color):
return RadialStatusIconContentNode(icon: .pause(color), synchronous: synchronous)
case let .customIcon(image):
return RadialStatusIconContentNode(icon: .custom(image), synchronous: synchronous)
case let .check(color):
return RadialCheckContentNode(color: color)
case let .progress(color, lineWidth, value, cancelEnabled, animateRotation):
if let current = current as? RadialProgressContentNode, current.displayCancel == cancelEnabled, current.animateRotation == animateRotation {
if !current.color.isEqual(color) {
current.color = color
}
current.progress = value
return current
} else {
let node = RadialProgressContentNode(color: color, lineWidth: lineWidth, displayCancel: cancelEnabled, animateRotation: animateRotation)
node.progress = value
return node
}
case let .cloudProgress(color, strokeLineColor, lineWidth, value):
if let current = current as? RadialCloudProgressContentNode {
if !current.color.isEqual(color) {
current.color = color
}
current.progress = value
return current
} else {
let node = RadialCloudProgressContentNode(color: color, backgroundStrokeColor: strokeLineColor, lineWidth: lineWidth)
node.progress = value
return node
}
case .staticTimeout:
return RadialStatusIconContentNode(icon: .timeout, synchronous: synchronous)
case let .secretTimeout(color, icon, beginTime, timeout, sparks):
var animate = true
if let current = current as? RadialStatusIconContentNode, case .timeout = current.icon {
animate = false
}
return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon, sparks: sparks, animate: animate)
}
}
}
public final class RadialStatusNode: ASControlNode {
public var backgroundNodeColor: UIColor {
didSet {
if self.backgroundNodeColor != oldValue {
self.transitionToBackgroundColor(self.state.backgroundColor(color: self.backgroundNodeColor), previousContentNode: nil, animated: false, synchronous: false, completion: {})
}
}
}
private let enableBlur: Bool
private let isPreview: Bool
public private(set) var state: RadialStatusNodeState = .none
private var staticBackgroundNode: ASImageNode?
private var backgroundNode: NavigationBackgroundNode?
private var currentBackgroundNodeColor: UIColor?
private var contentNode: RadialStatusContentNode?
private var nextContentNode: RadialStatusContentNode?
public init(backgroundNodeColor: UIColor, enableBlur: Bool = false, isPreview: Bool = false) {
self.backgroundNodeColor = backgroundNodeColor
self.enableBlur = enableBlur
self.isPreview = isPreview
super.init()
}
public func transitionToState(_ state: RadialStatusNodeState, animated: Bool = true, synchronous: Bool = false, completion: @escaping () -> Void = {}) {
var animated = animated
if self.state != state {
let fromState = self.state
self.state = state
if fromState.isPrimarilyEqual(to: state) {
animated = false
}
let contentNode = state.contentNode(current: self.contentNode, synchronous: synchronous)
if contentNode !== self.contentNode {
self.transitionToContentNode(contentNode, state: state, fromState: fromState, backgroundColor: state.backgroundColor(color: self.backgroundNodeColor), animated: animated, synchronous: synchronous, completion: completion)
} else {
self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), previousContentNode: nil, animated: animated, synchronous: synchronous, completion: completion)
}
} else {
completion()
}
}
private func transitionToContentNode(_ node: RadialStatusContentNode?, state: RadialStatusNodeState, fromState: RadialStatusNodeState, backgroundColor: UIColor?, animated: Bool, synchronous: Bool = false, completion: @escaping () -> Void) {
if let contentNode = self.contentNode {
self.nextContentNode = node
contentNode.enqueueReadyForTransition { [weak contentNode, weak self] in
if let strongSelf = self, let previousContentNode = contentNode, strongSelf.contentNode === contentNode {
if animated {
let nextContentNode = strongSelf.nextContentNode
strongSelf.contentNode = nextContentNode
previousContentNode.prepareAnimateOut(completion: { delay in
if let contentNode = strongSelf.contentNode, nextContentNode === contentNode {
strongSelf.addSubnode(contentNode)
contentNode.frame = strongSelf.bounds
contentNode.prepareAnimateIn(from: fromState)
if strongSelf.isNodeLoaded {
contentNode.layout()
contentNode.animateIn(from: fromState, delay: delay)
}
}
strongSelf.transitionToBackgroundColor(strongSelf.contentNode != nil ? backgroundColor : nil, previousContentNode: previousContentNode, animated: animated, synchronous: synchronous, completion: completion)
})
previousContentNode.animateOut(to: state, completion: { [weak contentNode] in
if let strongSelf = self, let contentNode = contentNode {
if contentNode !== strongSelf.contentNode {
contentNode.removeFromSupernode()
}
}
})
} else {
previousContentNode.removeFromSupernode()
strongSelf.contentNode = strongSelf.nextContentNode
if let contentNode = strongSelf.contentNode {
strongSelf.addSubnode(contentNode)
contentNode.frame = strongSelf.bounds
contentNode.prepareAnimateIn(from: fromState)
if strongSelf.isNodeLoaded {
contentNode.layout()
}
}
strongSelf.transitionToBackgroundColor(backgroundColor, previousContentNode: nil, animated: animated, synchronous: synchronous, completion: completion)
}
}
}
} else {
self.contentNode = node
if let contentNode = self.contentNode {
contentNode.displaysAsynchronously = self.displaysAsynchronously
contentNode.frame = self.bounds
contentNode.prepareAnimateIn(from: nil)
self.addSubnode(contentNode)
if animated, self.isNodeLoaded {
switch state {
case .check, .progress:
contentNode.layout()
contentNode.animateIn(from: fromState, delay: 0.0)
default:
break
}
}
}
self.transitionToBackgroundColor(backgroundColor, previousContentNode: nil, animated: animated, synchronous: synchronous, completion: completion)
}
}
private func transitionToBackgroundColor(_ color: UIColor?, previousContentNode: RadialStatusContentNode?, animated: Bool, synchronous: Bool, completion: @escaping () -> Void) {
let currentColor = self.currentBackgroundNodeColor
var updated = false
if let color = color, let currentColor = currentColor {
updated = !color.isEqual(currentColor)
} else if (currentColor != nil) != (color != nil) {
updated = true
}
if updated {
if let color = color {
if self.isPreview {
let backgroundNode: ASImageNode
if let current = self.staticBackgroundNode {
backgroundNode = current
} else {
backgroundNode = ASImageNode()
backgroundNode.image = generateFilledCircleImage(diameter: 50.0, color: self.backgroundNodeColor)
self.insertSubnode(backgroundNode, at: 0)
self.staticBackgroundNode = backgroundNode
}
backgroundNode.frame = self.bounds
} else {
if let backgroundNode = self.backgroundNode {
backgroundNode.updateColor(color: color, transition: .immediate)
self.currentBackgroundNodeColor = color
completion()
} else {
let backgroundNode = NavigationBackgroundNode(color: color, enableBlur: self.enableBlur)
self.currentBackgroundNodeColor = color
backgroundNode.frame = self.bounds
backgroundNode.update(size: backgroundNode.bounds.size, cornerRadius: backgroundNode.bounds.size.height / 2.0, transition: .immediate)
self.backgroundNode = backgroundNode
self.insertSubnode(backgroundNode, at: 0)
if animated {
backgroundNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2, removeOnCompletion: false)
backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
} else {
completion()
}
}
}
} else if let backgroundNode = self.backgroundNode {
self.backgroundNode = nil
self.currentBackgroundNodeColor = nil
if animated {
backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
previousContentNode?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak backgroundNode] _ in
backgroundNode?.removeFromSupernode()
completion()
})
} else {
backgroundNode.removeFromSupernode()
completion()
}
}
} else {
completion()
}
}
override public func layout() {
if let backgroundNode = self.backgroundNode {
backgroundNode.frame = self.bounds
backgroundNode.update(size: backgroundNode.bounds.size, cornerRadius: backgroundNode.bounds.size.height / 2.0, transition: .immediate)
}
if let contentNode = self.contentNode {
contentNode.frame = self.bounds
}
}
}
@@ -0,0 +1,291 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
import ManagedAnimationNode
private struct ContentParticle {
var position: CGPoint
var direction: CGPoint
var velocity: CGFloat
var alpha: CGFloat
var lifetime: Double
var beginTime: Double
init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) {
self.position = position
self.direction = direction
self.velocity = velocity
self.alpha = alpha
self.lifetime = lifetime
self.beginTime = beginTime
}
}
private final class RadialStatusSecretTimeoutContentNodeParameters: NSObject {
let color: UIColor
let icon: RadialStatusNodeState.SecretTimeoutIcon
let progress: CGFloat
let sparks: Bool
let particles: [ContentParticle]
let alphaProgress: CGFloat
init(color: UIColor, icon: RadialStatusNodeState.SecretTimeoutIcon, progress: CGFloat, sparks: Bool, particles: [ContentParticle], alphaProgress: CGFloat) {
self.color = color
self.icon = icon
self.progress = progress
self.sparks = sparks
self.particles = particles
self.alphaProgress = alphaProgress
}
}
final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private let beginTime: Double
private let timeout: Double
private let icon: RadialStatusNodeState.SecretTimeoutIcon
private let sparks: Bool
private var animationBeginTime: Double?
private var progress: CGFloat = 0.0
private var alphaProgress: CGFloat = 0.0
private var particles: [ContentParticle] = []
private var animationNode: FireIconNode?
private var displayLink: CADisplayLink?
init(color: UIColor, beginTime: Double, timeout: Double, icon: RadialStatusNodeState.SecretTimeoutIcon, sparks: Bool, animate: Bool = true) {
self.color = color
self.beginTime = beginTime
self.timeout = timeout
self.icon = icon
self.sparks = sparks
super.init()
self.isOpaque = false
class DisplayLinkProxy: NSObject {
weak var target: RadialStatusSecretTimeoutContentNode?
init(target: RadialStatusSecretTimeoutContentNode) {
self.target = target
}
@objc func displayLinkEvent() {
self.target?.displayLinkEvent()
}
}
self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
self.displayLink?.isPaused = true
self.displayLink?.add(to: RunLoop.main, forMode: .common)
if case .flame = icon {
if !animate {
self.animationBeginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
}
let animationNode = FireIconNode(animate: animate)
self.animationNode = animationNode
self.addSubnode(animationNode)
}
}
deinit {
self.displayLink?.invalidate()
}
override func layout() {
super.layout()
var factor: CGFloat = 0.75
var offset: CGFloat = 0.0415
if self.bounds.width < 30.0 {
factor = 0.66
offset = 0.08
}
let size = floorToScreenPixels(self.bounds.width * factor)
self.animationNode?.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size)
}
override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
completion()
})
}
override func animateIn(from: RadialStatusNodeState, delay: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
self.displayLink?.isPaused = false
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.displayLink?.isPaused = true
}
private func displayLinkEvent() {
let bounds = self.bounds
if bounds.width.isZero {
return
}
let absoluteTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let alphaProgress: CGFloat
if let animationBeginTime = self.animationBeginTime {
let fadeInDuration: Double = 0.4
alphaProgress = max(0.0, min(1.0, (absoluteTimestamp - animationBeginTime) / fadeInDuration))
} else {
alphaProgress = 1.0
}
var progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout))
if self.timeout == 0x7fffffff {
progress = 0.0
}
self.progress = progress
self.alphaProgress = alphaProgress
if self.sparks {
let lineWidth: CGFloat = 1.75
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius: CGFloat = (bounds.size.width - lineWidth - 2.5 * 2.0) * 0.5
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * self.progress
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
let timestamp = CACurrentMediaTime()
let dt: CGFloat = 1.0 / 60.0
var removeIndices: [Int] = []
for i in 0 ..< self.particles.count {
let currentTime = timestamp - self.particles[i].beginTime
if currentTime > self.particles[i].lifetime {
removeIndices.append(i)
} else {
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
self.particles[i].alpha = 1.0 - decelerated
var p = self.particles[i].position
let d = self.particles[i].direction
let v = self.particles[i].velocity
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
self.particles[i].position = p
}
}
for i in removeIndices.reversed() {
self.particles.remove(at: i)
}
let newParticleCount = 1
for _ in 0 ..< newParticleCount {
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 70.0
let angle: CGFloat = degrees * CGFloat.pi / 180.0
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.5
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01)
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
self.particles.append(particle)
}
}
self.setNeedsDisplay()
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialStatusSecretTimeoutContentNodeParameters(color: self.color, icon: self.icon, progress: self.progress, sparks: self.sparks, particles: self.particles, alphaProgress: self.alphaProgress)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialStatusSecretTimeoutContentNodeParameters {
var drawArc = true
if case let .image(icon) = parameters.icon, let iconImage = icon.cgImage {
let imageRect = CGRect(origin: CGPoint(x: floor((bounds.size.width - icon.size.width) / 2.0), y: floor((bounds.size.height - icon.size.height) / 2.0)), size: icon.size)
context.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.draw(iconImage, in: imageRect)
context.restoreGState()
drawArc = false
}
let lineWidth: CGFloat
if parameters.sparks {
lineWidth = 1.75
} else {
lineWidth = 1.75
}
context.setFillColor(parameters.color.cgColor)
context.setStrokeColor(parameters.color.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
context.setLineJoin(.miter)
context.setMiterLimit(10.0)
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius: CGFloat = (bounds.size.width - lineWidth - 2.5 * 2.0) * 0.5
let startAngle: CGFloat = -CGFloat.pi / 2.0
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * parameters.progress
if drawArc {
context.setAlpha(parameters.alphaProgress)
let path = CGMutablePath()
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
context.addPath(path)
context.strokePath()
}
for particle in parameters.particles {
let size: CGFloat = 1.3
context.setAlpha(particle.alpha * parameters.alphaProgress)
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
}
}
}
}
final class FireIconNode: ManagedAnimationNode {
init(animate: Bool) {
super.init(size: CGSize(width: 100.0, height: 100.0))
if animate {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 0, endFrame: 120), duration: 2.0))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 120, endFrame: 120), duration: 0.001))
}
}
}