mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
403 lines
19 KiB
Swift
403 lines
19 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AppBundle
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SolidRoundedButtonNode
|
|
import SwiftSignalKit
|
|
import OverlayStatusController
|
|
import AnimationUI
|
|
import AccountContext
|
|
import TelegramPresentationData
|
|
import PresentationDataUtils
|
|
import TelegramCore
|
|
import Markdown
|
|
import DeviceAccess
|
|
|
|
private let colorKeyRegex = try? NSRegularExpression(pattern: "\"k\":\\[[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\]")
|
|
|
|
private func transformedWithTheme(data: Data, theme: PresentationTheme) -> Data {
|
|
if var string = String(data: data, encoding: .utf8) {
|
|
var colors: [UIColor] = [0x333333, 0xFFFFFF, 0x50A7EA, 0x212121].map { UIColor(rgb: $0) }
|
|
let replacementColors: [UIColor] = [theme.list.itemPrimaryTextColor.mixedWith(.white, alpha: 0.2), theme.list.plainBackgroundColor, theme.list.itemAccentColor, theme.list.plainBackgroundColor]
|
|
|
|
func colorToString(_ color: UIColor) -> String {
|
|
var r: CGFloat = 0.0
|
|
var g: CGFloat = 0.0
|
|
var b: CGFloat = 0.0
|
|
if color.getRed(&r, green: &g, blue: &b, alpha: nil) {
|
|
return "\"k\":[\(r),\(g),\(b),1]"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func match(_ a: Double, _ b: Double, eps: Double) -> Bool {
|
|
return abs(a - b) < eps
|
|
}
|
|
|
|
var replacements: [(NSTextCheckingResult, String)] = []
|
|
|
|
if let colorKeyRegex = colorKeyRegex {
|
|
let results = colorKeyRegex.matches(in: string, range: NSRange(string.startIndex..., in: string))
|
|
for result in results.reversed() {
|
|
if let range = Range(result.range, in: string) {
|
|
let substring = String(string[range])
|
|
let color = substring[substring.index(string.startIndex, offsetBy: "\"k\":[".count) ..< substring.index(before: substring.endIndex)]
|
|
let components = color.split(separator: ",")
|
|
if components.count == 4, let r = Double(components[0]), let g = Double(components[1]), let b = Double(components[2]), let a = Double(components[3]) {
|
|
if match(a, 1.0, eps: 0.01) {
|
|
for i in 0 ..< colors.count {
|
|
let color = colors[i]
|
|
var cr: CGFloat = 0.0
|
|
var cg: CGFloat = 0.0
|
|
var cb: CGFloat = 0.0
|
|
if color.getRed(&cr, green: &cg, blue: &cb, alpha: nil) {
|
|
if match(r, Double(cr), eps: 0.01) && match(g, Double(cg), eps: 0.01) && match(b, Double(cb), eps: 0.01) {
|
|
replacements.append((result, colorToString(replacementColors[i])))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (result, text) in replacements {
|
|
if let range = Range(result.range, in: string) {
|
|
string = string.replacingCharacters(in: range, with: text)
|
|
}
|
|
}
|
|
|
|
return string.data(using: .utf8) ?? data
|
|
} else {
|
|
return data
|
|
}
|
|
}
|
|
|
|
public final class AuthDataTransferSplashScreen: ViewController {
|
|
private let context: AccountContext
|
|
private let activeSessionsContext: ActiveSessionsContext
|
|
private var presentationData: PresentationData
|
|
|
|
public init(context: AccountContext, activeSessionsContext: ActiveSessionsContext) {
|
|
self.context = context
|
|
self.activeSessionsContext = activeSessionsContext
|
|
|
|
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme)
|
|
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor)
|
|
|
|
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close)))
|
|
|
|
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
|
self.navigationPresentation = .modalInLargeLayout
|
|
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
|
self.navigationBar?.intrinsicCanTransitionInline = false
|
|
|
|
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
|
}
|
|
|
|
required init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
override public func loadDisplayNode() {
|
|
self.displayNode = AuthDataTransferSplashScreenNode(context: self.context, presentationData: self.presentationData, action: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
DeviceAccess.authorizeAccess(to: .camera, presentationData: strongSelf.presentationData, present: { c, a in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
c.presentationArguments = a
|
|
strongSelf.context.sharedContext.mainWindow?.present(c, on: .root)
|
|
}, openSettings: {
|
|
self?.context.sharedContext.applicationBindings.openSettings()
|
|
}, { granted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
guard granted else {
|
|
return
|
|
}
|
|
(strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: AuthTransferScanScreen(context: strongSelf.context, activeSessionsContext: strongSelf.activeSessionsContext), animated: true)
|
|
})
|
|
})
|
|
|
|
self.displayNodeDidLoad()
|
|
}
|
|
|
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
|
|
(self.displayNode as! AuthDataTransferSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode {
|
|
private var presentationData: PresentationData
|
|
|
|
private var animationSize: CGSize = CGSize()
|
|
private var animationOffset: CGPoint = CGPoint()
|
|
private let animationNode: AnimationNode?
|
|
private let titleNode: ImmediateTextNode
|
|
private let badgeBackgroundNodes: [ASImageNode]
|
|
private let badgeTextNodes: [ImmediateTextNode]
|
|
private let textNodes: [ImmediateTextNode]
|
|
let buttonNode: SolidRoundedButtonNode
|
|
|
|
private let hierarchyTrackingNode: HierarchyTrackingNode
|
|
|
|
var inProgress: Bool = false {
|
|
didSet {
|
|
self.buttonNode.isUserInteractionEnabled = !self.inProgress
|
|
self.buttonNode.alpha = self.inProgress ? 0.6 : 1.0
|
|
}
|
|
}
|
|
|
|
private var validLayout: ContainerViewLayout?
|
|
|
|
init(context: AccountContext, presentationData: PresentationData, action: @escaping () -> Void) {
|
|
self.presentationData = presentationData
|
|
|
|
if let url = getAppBundle().url(forResource: "anim_qr", withExtension: "json"), let data = try? Data(contentsOf: url) {
|
|
self.animationNode = AnimationNode(animationData: transformedWithTheme(data: data, theme: presentationData.theme))
|
|
} else {
|
|
self.animationNode = nil
|
|
}
|
|
|
|
let buttonText: String
|
|
|
|
let badgeFont = Font.with(size: 13.0, design: .round, traits: [.bold])
|
|
let textFont = Font.regular(16.0)
|
|
let textColor = self.presentationData.theme.list.itemPrimaryTextColor
|
|
|
|
var badgeBackgroundNodes: [ASImageNode] = []
|
|
var badgeTextNodes: [ImmediateTextNode] = []
|
|
var textNodes: [ImmediateTextNode] = []
|
|
|
|
let badgeBackground = generateFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.list.itemCheckColors.fillColor)
|
|
|
|
for i in 0 ..< 3 {
|
|
let badgeBackgroundNode = ASImageNode()
|
|
badgeBackgroundNode.displaysAsynchronously = false
|
|
badgeBackgroundNode.displayWithoutProcessing = true
|
|
badgeBackgroundNode.image = badgeBackground
|
|
badgeBackgroundNodes.append(badgeBackgroundNode)
|
|
|
|
let badgeTextNode = ImmediateTextNode()
|
|
badgeTextNode.displaysAsynchronously = false
|
|
badgeTextNode.attributedText = NSAttributedString(string: "\(i + 1)", font: badgeFont, textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .natural)
|
|
badgeTextNode.maximumNumberOfLines = 0
|
|
badgeTextNode.lineSpacing = 0.1
|
|
badgeTextNodes.append(badgeTextNode)
|
|
|
|
let string: String
|
|
switch i {
|
|
case 0:
|
|
string = self.presentationData.strings.AuthSessions_AddDeviceIntro_Text1
|
|
case 1:
|
|
string = self.presentationData.strings.AuthSessions_AddDeviceIntro_Text2
|
|
default:
|
|
string = self.presentationData.strings.AuthSessions_AddDeviceIntro_Text3
|
|
}
|
|
|
|
let body = MarkdownAttributeSet(font: textFont, textColor: textColor)
|
|
let link = MarkdownAttributeSet(font: textFont, textColor: self.presentationData.theme.list.itemAccentColor, additionalAttributes: ["URL": true as NSNumber])
|
|
|
|
let text = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in
|
|
return nil
|
|
}))
|
|
|
|
let textNode = ImmediateTextNode()
|
|
textNode.displaysAsynchronously = false
|
|
textNode.attributedText = text
|
|
textNode.maximumNumberOfLines = 0
|
|
textNode.lineSpacing = 0.1
|
|
textNodes.append(textNode)
|
|
}
|
|
|
|
self.badgeBackgroundNodes = badgeBackgroundNodes
|
|
self.badgeTextNodes = badgeTextNodes
|
|
self.textNodes = textNodes
|
|
|
|
buttonText = self.presentationData.strings.AuthSessions_AddDeviceIntro_Action
|
|
|
|
self.titleNode = ImmediateTextNode()
|
|
self.titleNode.displaysAsynchronously = false
|
|
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_AddDeviceIntro_Title, font: Font.bold(24.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
self.titleNode.maximumNumberOfLines = 0
|
|
self.titleNode.textAlignment = .center
|
|
|
|
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 10.0, gloss: false)
|
|
self.buttonNode.isHidden = buttonText.isEmpty
|
|
|
|
var updateInHierarchy: ((Bool) -> Void)?
|
|
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
|
|
updateInHierarchy?(value)
|
|
})
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
|
|
self.addSubnode(self.hierarchyTrackingNode)
|
|
|
|
if let animationNode = self.animationNode {
|
|
self.addSubnode(animationNode)
|
|
}
|
|
self.addSubnode(self.titleNode)
|
|
|
|
self.badgeBackgroundNodes.forEach(self.addSubnode)
|
|
self.badgeTextNodes.forEach(self.addSubnode)
|
|
self.textNodes.forEach(self.addSubnode)
|
|
|
|
self.addSubnode(self.buttonNode)
|
|
|
|
self.buttonNode.pressed = {
|
|
action()
|
|
}
|
|
|
|
for textNode in self.textNodes {
|
|
textNode.linkHighlightColor = self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5)
|
|
textNode.highlightAttributeAction = { attributes in
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
|
return NSAttributedString.Key(rawValue: "URL")
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
textNode.tapAttributeAction = { attributes, _ in
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
|
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://desktop.telegram.org", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
|
|
}
|
|
}
|
|
}
|
|
|
|
updateInHierarchy = { [weak self] value in
|
|
if value {
|
|
self?.animationNode?.play()
|
|
} else {
|
|
self?.animationNode?.reset()
|
|
}
|
|
}
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
}
|
|
|
|
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
let firstTime = self.validLayout == nil
|
|
self.validLayout = layout
|
|
|
|
let sideInset: CGFloat = 22.0
|
|
let textSideInset: CGFloat = 54.0
|
|
let buttonSideInset: CGFloat = 16.0
|
|
let titleSpacing: CGFloat = 25.0
|
|
let buttonHeight: CGFloat = 50.0
|
|
let buttonSpacing: CGFloat = 16.0
|
|
let textSpacing: CGFloat = 25.0
|
|
let badgeSize: CGFloat = 20.0
|
|
|
|
let animationFitSize = CGSize(width: min(500.0, layout.size.width - sideInset + 20.0), height: 500.0)
|
|
let animationSize = self.animationNode?.preferredSize()?.fitted(animationFitSize) ?? animationFitSize
|
|
let iconSize: CGSize = animationSize
|
|
var iconOffset = CGPoint()
|
|
|
|
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
|
|
|
|
let hasRTL = self.badgeTextNodes.first?.cachedLayout?.hasRTL ?? false
|
|
|
|
var badgeTextSizes: [CGSize] = []
|
|
var textSizes: [CGSize] = []
|
|
var textContentHeight: CGFloat = 0.0
|
|
for i in 0 ..< self.badgeTextNodes.count {
|
|
let badgeTextSize = self.badgeTextNodes[i].updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude))
|
|
badgeTextSizes.append(badgeTextSize)
|
|
let textSize = self.textNodes[i].updateLayout(CGSize(width: layout.size.width - sideInset * 2.0 - 40.0, height: .greatestFiniteMagnitude))
|
|
textSizes.append(textSize)
|
|
|
|
if i != 0 {
|
|
textContentHeight += textSpacing
|
|
}
|
|
textContentHeight += textSize.height
|
|
}
|
|
|
|
var contentHeight = iconSize.height + titleSize.height + titleSpacing + textContentHeight
|
|
|
|
let bottomInset = layout.intrinsicInsets.bottom + 20.0
|
|
let contentTopInset = navigationHeight
|
|
let contentBottomInset = bottomInset + buttonHeight + buttonSpacing
|
|
|
|
let iconSpacing: CGFloat = max(20.0, min(61.0, layout.size.height - contentTopInset - contentBottomInset - contentHeight - 40.0))
|
|
|
|
contentHeight += iconSpacing
|
|
|
|
var contentVerticalOrigin = contentTopInset + floor((layout.size.height - contentTopInset - contentBottomInset - contentHeight) / 2.0)
|
|
|
|
let buttonWidth = layout.size.width - buttonSideInset * 2.0
|
|
|
|
let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight))
|
|
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
|
|
self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
|
|
|
|
var maxContentVerticalOrigin = buttonFrame.minY - 12.0 - contentHeight
|
|
|
|
contentVerticalOrigin = min(contentVerticalOrigin, maxContentVerticalOrigin)
|
|
|
|
var contentY = contentVerticalOrigin
|
|
let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0) + self.animationOffset.x, y: contentY), size: iconSize).offsetBy(dx: iconOffset.x, dy: iconOffset.y)
|
|
contentY += iconSize.height + iconSpacing
|
|
if let animationNode = self.animationNode {
|
|
transition.updateFrameAdditive(node: animationNode, frame: iconFrame)
|
|
if iconFrame.minY < 0.0 {
|
|
transition.updateAlpha(node: animationNode, alpha: 0.0)
|
|
} else {
|
|
transition.updateAlpha(node: animationNode, alpha: 1.0)
|
|
}
|
|
}
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: contentY), size: titleSize)
|
|
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
|
|
contentY += titleSize.height + titleSpacing
|
|
|
|
for i in 0 ..< self.badgeTextNodes.count {
|
|
if i != 0 {
|
|
contentY += textSpacing
|
|
}
|
|
|
|
let badgeTextSize = badgeTextSizes[i]
|
|
let textSize = textSizes[i]
|
|
|
|
let textFrame = CGRect(origin: CGPoint(x: textSideInset, y: contentY), size: textSize)
|
|
transition.updateFrameAdditive(node: self.textNodes[i], frame: textFrame)
|
|
|
|
let badgeFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.minY), size: CGSize(width: badgeSize, height: badgeSize))
|
|
transition.updateFrameAdditive(node: self.badgeBackgroundNodes[i], frame: badgeFrame)
|
|
|
|
let badgeTextOffsetX: CGFloat
|
|
if i == 0 {
|
|
badgeTextOffsetX = 0.5
|
|
} else {
|
|
badgeTextOffsetX = 1.0
|
|
}
|
|
|
|
transition.updateFrameAdditive(node: self.badgeTextNodes[i], frame: CGRect(origin: CGPoint(x: badgeFrame.minX + floor((badgeFrame.width - badgeTextSize.width) / 2.0) + badgeTextOffsetX, y: badgeFrame.minY + floor((badgeFrame.height - badgeTextSize.height) / 2.0) + 0.5), size: badgeTextSize))
|
|
|
|
contentY += textSize.height
|
|
}
|
|
|
|
if firstTime {
|
|
self.animationNode?.play()
|
|
}
|
|
}
|
|
}
|