mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge commit '076129348bdc195950533edc8d8d9fdbe2312ad7'
This commit is contained in:
commit
2c761951b2
@ -1,37 +0,0 @@
|
|||||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
|
||||||
|
|
||||||
swift_library(
|
|
||||||
name = "AuthTransferUI",
|
|
||||||
module_name = "AuthTransferUI",
|
|
||||||
srcs = glob([
|
|
||||||
"Sources/**/*.swift",
|
|
||||||
]),
|
|
||||||
copts = [
|
|
||||||
"-warnings-as-errors",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
"//submodules/TelegramCore:TelegramCore",
|
|
||||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
|
||||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
|
||||||
"//submodules/Display:Display",
|
|
||||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
|
||||||
"//submodules/AccountContext:AccountContext",
|
|
||||||
"//submodules/QrCode:QrCode",
|
|
||||||
"//submodules/Camera:Camera",
|
|
||||||
"//submodules/GlassButtonNode:GlassButtonNode",
|
|
||||||
"//submodules/AlertUI:AlertUI",
|
|
||||||
"//submodules/AppBundle:AppBundle",
|
|
||||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
|
||||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
|
||||||
"//submodules/AnimationUI:AnimationUI",
|
|
||||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
|
||||||
"//submodules/DeviceAccess:DeviceAccess",
|
|
||||||
"//submodules/UndoUI:UndoUI",
|
|
||||||
"//submodules/TextFormat:TextFormat",
|
|
||||||
"//submodules/Markdown:Markdown",
|
|
||||||
"//submodules/QrCodeUI:QrCodeUI",
|
|
||||||
],
|
|
||||||
visibility = [
|
|
||||||
"//visibility:public",
|
|
||||||
],
|
|
||||||
)
|
|
@ -1,343 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import AppBundle
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import Display
|
|
||||||
import SolidRoundedButtonNode
|
|
||||||
import SwiftSignalKit
|
|
||||||
import AnimationUI
|
|
||||||
import AccountContext
|
|
||||||
import TelegramPresentationData
|
|
||||||
import PresentationDataUtils
|
|
||||||
import TelegramCore
|
|
||||||
import Markdown
|
|
||||||
import DeviceAccess
|
|
||||||
import QrCodeUI
|
|
||||||
|
|
||||||
private func transformedWithTheme(data: Data, theme: PresentationTheme) -> Data {
|
|
||||||
return transformedWithColors(data: data, colors: [(UIColor(rgb: 0x333333), theme.list.itemPrimaryTextColor.mixedWith(.white, alpha: 0.2)), (UIColor(rgb: 0xFFFFFF), theme.list.plainBackgroundColor), (UIColor(rgb: 0x50A7EA), theme.list.itemAccentColor), (UIColor(rgb: 0x212121), theme.list.plainBackgroundColor)])
|
|
||||||
}
|
|
||||||
|
|
||||||
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, enableBackgroundBlur: false, 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(.qrCode), 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: QrCodeScanScreen(context: strongSelf.context, subject: .authTransfer(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.navigationLayout(layout: layout).navigationFrame.maxY, 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, weight: .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
|
|
||||||
let iconOffset = CGPoint()
|
|
||||||
|
|
||||||
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
|
|
||||||
|
|
||||||
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)
|
|
||||||
let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
|
|
||||||
|
|
||||||
let 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,544 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import AccountContext
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import Display
|
|
||||||
import SwiftSignalKit
|
|
||||||
import Camera
|
|
||||||
import GlassButtonNode
|
|
||||||
import CoreImage
|
|
||||||
import AlertUI
|
|
||||||
import TelegramPresentationData
|
|
||||||
import TelegramCore
|
|
||||||
import UndoUI
|
|
||||||
import Markdown
|
|
||||||
import TextFormat
|
|
||||||
|
|
||||||
private func parseAuthTransferUrl(_ url: URL) -> Data? {
|
|
||||||
var tokenString: String?
|
|
||||||
if let query = url.query, let components = URLComponents(string: "/?" + query), let queryItems = components.queryItems {
|
|
||||||
for queryItem in queryItems {
|
|
||||||
if let value = queryItem.value {
|
|
||||||
if queryItem.name == "token", !value.isEmpty {
|
|
||||||
tokenString = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if var tokenString = tokenString {
|
|
||||||
tokenString = tokenString.replacingOccurrences(of: "-", with: "+")
|
|
||||||
tokenString = tokenString.replacingOccurrences(of: "_", with: "/")
|
|
||||||
while tokenString.count % 4 != 0 {
|
|
||||||
tokenString.append("=")
|
|
||||||
}
|
|
||||||
if let data = Data(base64Encoded: tokenString) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateFrameImage() -> UIImage? {
|
|
||||||
return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in
|
|
||||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
|
||||||
context.clear(bounds)
|
|
||||||
context.setStrokeColor(UIColor.white.cgColor)
|
|
||||||
context.setLineWidth(4.0)
|
|
||||||
context.setLineCap(.round)
|
|
||||||
|
|
||||||
let path = CGMutablePath()
|
|
||||||
path.move(to: CGPoint(x: 2.0, y: 2.0 + 26.0))
|
|
||||||
path.addArc(tangent1End: CGPoint(x: 2.0, y: 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: 2.0), radius: 6.0)
|
|
||||||
path.addLine(to: CGPoint(x: 2.0 + 26.0, y: 2.0))
|
|
||||||
context.addPath(path)
|
|
||||||
context.strokePath()
|
|
||||||
|
|
||||||
path.move(to: CGPoint(x: size.width - 2.0, y: 2.0 + 26.0))
|
|
||||||
path.addArc(tangent1End: CGPoint(x: size.width - 2.0, y: 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: 2.0), radius: 6.0)
|
|
||||||
path.addLine(to: CGPoint(x: size.width - 2.0 - 26.0, y: 2.0))
|
|
||||||
context.addPath(path)
|
|
||||||
context.strokePath()
|
|
||||||
|
|
||||||
path.move(to: CGPoint(x: 2.0, y: size.height - 2.0 - 26.0))
|
|
||||||
path.addArc(tangent1End: CGPoint(x: 2.0, y: size.height - 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0), radius: 6.0)
|
|
||||||
path.addLine(to: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0))
|
|
||||||
context.addPath(path)
|
|
||||||
context.strokePath()
|
|
||||||
|
|
||||||
path.move(to: CGPoint(x: size.width - 2.0, y: size.height - 2.0 - 26.0))
|
|
||||||
path.addArc(tangent1End: CGPoint(x: size.width - 2.0, y: size.height - 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0), radius: 6.0)
|
|
||||||
path.addLine(to: CGPoint(x: size.width - 2.0 - 26.0, y: size.height - 2.0))
|
|
||||||
context.addPath(path)
|
|
||||||
context.strokePath()
|
|
||||||
})?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class AuthTransferScanScreen: ViewController {
|
|
||||||
private let context: AccountContext
|
|
||||||
private let activeSessionsContext: ActiveSessionsContext
|
|
||||||
private var presentationData: PresentationData
|
|
||||||
|
|
||||||
private var codeDisposable: Disposable?
|
|
||||||
private var inForegroundDisposable: Disposable?
|
|
||||||
private let approveDisposable = MetaDisposable()
|
|
||||||
|
|
||||||
private var controllerNode: AuthTransferScanScreenNode {
|
|
||||||
return self.displayNode as! AuthTransferScanScreenNode
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(context: AccountContext, activeSessionsContext: ActiveSessionsContext) {
|
|
||||||
self.context = context
|
|
||||||
self.activeSessionsContext = activeSessionsContext
|
|
||||||
|
|
||||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
|
|
||||||
let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
|
||||||
|
|
||||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close)))
|
|
||||||
|
|
||||||
self.statusBar.statusBarStyle = .White
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
self.inForegroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground
|
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] inForeground in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
(strongSelf.displayNode as! AuthTransferScanScreenNode).updateInForeground(inForeground)
|
|
||||||
})
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Test", style: .plain, target: self, action: #selector(self.testPressed))
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
self.codeDisposable?.dispose()
|
|
||||||
self.inForegroundDisposable?.dispose()
|
|
||||||
self.approveDisposable.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func testPressed() {
|
|
||||||
self.dismissWithSuccess(session: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func dismissWithSuccess(session: RecentAccountSession?) {
|
|
||||||
if let navigationController = navigationController as? NavigationController {
|
|
||||||
let activeSessionsContext = self.activeSessionsContext
|
|
||||||
|
|
||||||
self.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: self.presentationData.strings.AuthSessions_AddedDeviceTitle, text: session?.appName ?? "Telegram for macOS", cancel: self.presentationData.strings.AuthSessions_AddedDeviceTerminate), elevatedLayout: false, animateInAsReplacement: false, action: { value in
|
|
||||||
if value == .undo, let session = session {
|
|
||||||
let _ = activeSessionsContext.remove(hash: session.hash).start()
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}), in: .window(.root))
|
|
||||||
|
|
||||||
var viewControllers = navigationController.viewControllers
|
|
||||||
viewControllers = viewControllers.filter { controller in
|
|
||||||
if controller is RecentSessionsController {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if controller === self {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
viewControllers.append(self.context.sharedContext.makeRecentSessionsController(context: self.context, activeSessionsContext: activeSessionsContext))
|
|
||||||
navigationController.setViewControllers(viewControllers, animated: true)
|
|
||||||
} else {
|
|
||||||
self.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func loadDisplayNode() {
|
|
||||||
self.displayNode = AuthTransferScanScreenNode(context: self.context, presentationData: self.presentationData)
|
|
||||||
|
|
||||||
self.displayNodeDidLoad()
|
|
||||||
|
|
||||||
self.codeDisposable = ((self.displayNode as! AuthTransferScanScreenNode).focusedCode.get()
|
|
||||||
|> map { code -> String? in
|
|
||||||
return code?.message
|
|
||||||
}
|
|
||||||
|> distinctUntilChanged
|
|
||||||
|> mapToSignal { code -> Signal<String?, NoError> in
|
|
||||||
return .single(code)
|
|
||||||
|> delay(0.5, queue: Queue.mainQueue())
|
|
||||||
}).start(next: { [weak self] code in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let code = code else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let url = URL(string: code), let parsedToken = parseAuthTransferUrl(url) {
|
|
||||||
strongSelf.approveDisposable.set((approveAuthTransferToken(account: strongSelf.context.account, token: parsedToken, activeSessionsContext: strongSelf.activeSessionsContext)
|
|
||||||
|> deliverOnMainQueue).start(next: { session in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
strongSelf.controllerNode.codeWithError = nil
|
|
||||||
let activeSessionsContext = strongSelf.activeSessionsContext
|
|
||||||
Queue.mainQueue().after(1.5, {
|
|
||||||
activeSessionsContext.loadMore()
|
|
||||||
})
|
|
||||||
strongSelf.dismissWithSuccess(session: session)
|
|
||||||
}, error: { _ in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
strongSelf.controllerNode.codeWithError = code
|
|
||||||
strongSelf.controllerNode.updateFocusedRect(nil)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
||||||
super.containerLayoutUpdated(layout, transition: transition)
|
|
||||||
|
|
||||||
(self.displayNode as! AuthTransferScanScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class AuthTransferScanScreenNode: ViewControllerTracingNode, UIScrollViewDelegate {
|
|
||||||
private let context: AccountContext
|
|
||||||
private var presentationData: PresentationData
|
|
||||||
|
|
||||||
private let previewNode: CameraPreviewNode
|
|
||||||
private let fadeNode: ASDisplayNode
|
|
||||||
private let topDimNode: ASDisplayNode
|
|
||||||
private let bottomDimNode: ASDisplayNode
|
|
||||||
private let leftDimNode: ASDisplayNode
|
|
||||||
private let rightDimNode: ASDisplayNode
|
|
||||||
private let centerDimNode: ASDisplayNode
|
|
||||||
private let frameNode: ASImageNode
|
|
||||||
private let torchButtonNode: GlassButtonNode
|
|
||||||
private let titleNode: ImmediateTextNode
|
|
||||||
private let textNode: ImmediateTextNode
|
|
||||||
private let errorTextNode: ImmediateTextNode
|
|
||||||
|
|
||||||
private let camera: Camera
|
|
||||||
private let codeDisposable = MetaDisposable()
|
|
||||||
private var torchDisposable: Disposable?
|
|
||||||
|
|
||||||
fileprivate let focusedCode = ValuePromise<CameraCode?>(ignoreRepeated: true)
|
|
||||||
private var focusedRect: CGRect?
|
|
||||||
|
|
||||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
|
||||||
|
|
||||||
var codeWithError: String? {
|
|
||||||
didSet {
|
|
||||||
if self.codeWithError != oldValue {
|
|
||||||
if self.codeWithError != nil {
|
|
||||||
self.errorTextNode.isHidden = false
|
|
||||||
} else {
|
|
||||||
self.errorTextNode.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var highlightViews: [UIVisualEffectView] = []
|
|
||||||
|
|
||||||
init(context: AccountContext, presentationData: PresentationData) {
|
|
||||||
self.context = context
|
|
||||||
self.presentationData = presentationData
|
|
||||||
|
|
||||||
self.previewNode = CameraPreviewNode()
|
|
||||||
self.previewNode.backgroundColor = .black
|
|
||||||
|
|
||||||
self.fadeNode = ASDisplayNode()
|
|
||||||
self.fadeNode.alpha = 0.0
|
|
||||||
self.fadeNode.backgroundColor = .black
|
|
||||||
|
|
||||||
self.topDimNode = ASDisplayNode()
|
|
||||||
self.topDimNode.alpha = 0.625
|
|
||||||
self.topDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
|
||||||
|
|
||||||
self.bottomDimNode = ASDisplayNode()
|
|
||||||
self.bottomDimNode.alpha = 0.625
|
|
||||||
self.bottomDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
|
||||||
|
|
||||||
self.leftDimNode = ASDisplayNode()
|
|
||||||
self.leftDimNode.alpha = 0.625
|
|
||||||
self.leftDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
|
||||||
|
|
||||||
self.rightDimNode = ASDisplayNode()
|
|
||||||
self.rightDimNode.alpha = 0.625
|
|
||||||
self.rightDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
|
||||||
|
|
||||||
self.centerDimNode = ASDisplayNode()
|
|
||||||
self.centerDimNode.alpha = 0.0
|
|
||||||
self.centerDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
|
||||||
|
|
||||||
self.frameNode = ASImageNode()
|
|
||||||
self.frameNode.image = generateFrameImage()
|
|
||||||
|
|
||||||
self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil)
|
|
||||||
|
|
||||||
self.titleNode = ImmediateTextNode()
|
|
||||||
self.titleNode.displaysAsynchronously = false
|
|
||||||
self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.AuthSessions_AddDevice_ScanTitle, font: Font.bold(32.0), textColor: .white)
|
|
||||||
self.titleNode.maximumNumberOfLines = 0
|
|
||||||
self.titleNode.textAlignment = .center
|
|
||||||
|
|
||||||
let textFont = Font.regular(17.0)
|
|
||||||
let boldFont = Font.bold(17.0)
|
|
||||||
|
|
||||||
var text = presentationData.strings.AuthSessions_AddDevice_ScanInstallInfo
|
|
||||||
text = text.replacingOccurrences(of: " [", with: " [").replacingOccurrences(of: ") ", with: ") ")
|
|
||||||
|
|
||||||
let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: .white), bold: MarkdownAttributeSet(font: boldFont, textColor: .white), link: MarkdownAttributeSet(font: boldFont, textColor: .white), linkAttribute: { contents in
|
|
||||||
return (TelegramTextAttributes.URL, contents)
|
|
||||||
})))
|
|
||||||
|
|
||||||
self.textNode = ImmediateTextNode()
|
|
||||||
self.textNode.displaysAsynchronously = false
|
|
||||||
self.textNode.attributedText = attributedText
|
|
||||||
self.textNode.maximumNumberOfLines = 0
|
|
||||||
self.textNode.textAlignment = .center
|
|
||||||
self.textNode.lineSpacing = 0.5
|
|
||||||
|
|
||||||
self.errorTextNode = ImmediateTextNode()
|
|
||||||
self.errorTextNode.displaysAsynchronously = false
|
|
||||||
self.errorTextNode.attributedText = NSAttributedString(string: presentationData.strings.AuthSessions_AddDevice_InvalidQRCode, font: Font.medium(16.0), textColor: .white)
|
|
||||||
self.errorTextNode.maximumNumberOfLines = 0
|
|
||||||
self.errorTextNode.textAlignment = .center
|
|
||||||
self.errorTextNode.isHidden = true
|
|
||||||
|
|
||||||
self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false))
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
||||||
|
|
||||||
self.torchDisposable = (self.camera.hasTorch
|
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] hasTorch in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.torchButtonNode.isHidden = !hasTorch
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
self.addSubnode(self.previewNode)
|
|
||||||
self.addSubnode(self.fadeNode)
|
|
||||||
self.addSubnode(self.topDimNode)
|
|
||||||
self.addSubnode(self.bottomDimNode)
|
|
||||||
self.addSubnode(self.leftDimNode)
|
|
||||||
self.addSubnode(self.rightDimNode)
|
|
||||||
self.addSubnode(self.centerDimNode)
|
|
||||||
self.addSubnode(self.frameNode)
|
|
||||||
self.addSubnode(self.torchButtonNode)
|
|
||||||
self.addSubnode(self.titleNode)
|
|
||||||
self.addSubnode(self.textNode)
|
|
||||||
self.addSubnode(self.errorTextNode)
|
|
||||||
|
|
||||||
self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
self.codeDisposable.dispose()
|
|
||||||
self.torchDisposable?.dispose()
|
|
||||||
self.camera.stopCapture(invalidate: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func updateInForeground(_ inForeground: Bool) {
|
|
||||||
if !inForeground {
|
|
||||||
self.camera.stopCapture(invalidate: false)
|
|
||||||
} else {
|
|
||||||
self.camera.startCapture()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func didLoad() {
|
|
||||||
super.didLoad()
|
|
||||||
|
|
||||||
self.camera.attachPreviewNode(self.previewNode)
|
|
||||||
self.camera.startCapture()
|
|
||||||
|
|
||||||
let throttledSignal = self.camera.detectedCodes
|
|
||||||
|> mapToThrottled { next -> Signal<[CameraCode], NoError> in
|
|
||||||
return .single(next) |> then(.complete() |> delay(0.3, queue: Queue.concurrentDefaultQueue()))
|
|
||||||
}
|
|
||||||
|
|
||||||
self.codeDisposable.set((throttledSignal
|
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] codes in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let filteredCodes = codes.filter { $0.message.hasPrefix("tg://") }
|
|
||||||
if let code = filteredCodes.first, CGRect(x: 0.3, y: 0.3, width: 0.4, height: 0.4).contains(code.boundingBox.center) {
|
|
||||||
if strongSelf.codeWithError != code.message {
|
|
||||||
strongSelf.codeWithError = nil
|
|
||||||
}
|
|
||||||
if strongSelf.codeWithError == code.message {
|
|
||||||
strongSelf.focusedCode.set(nil)
|
|
||||||
strongSelf.updateFocusedRect(nil)
|
|
||||||
} else {
|
|
||||||
strongSelf.focusedCode.set(code)
|
|
||||||
strongSelf.updateFocusedRect(code.boundingBox)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
strongSelf.codeWithError = nil
|
|
||||||
strongSelf.focusedCode.set(nil)
|
|
||||||
strongSelf.updateFocusedRect(nil)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
||||||
recognizer.tapActionAtPoint = { _ in
|
|
||||||
return .waitForSingleTap
|
|
||||||
}
|
|
||||||
self.textNode.view.addGestureRecognizer(recognizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
||||||
switch recognizer.state {
|
|
||||||
case .ended:
|
|
||||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
|
||||||
switch gesture {
|
|
||||||
case .tap:
|
|
||||||
if let (_, attributes) = self.textNode.attributesAtPoint(location) {
|
|
||||||
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
||||||
switch url {
|
|
||||||
case "desktop":
|
|
||||||
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: "https://getdesktop.telegram.org", forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
|
|
||||||
case "web":
|
|
||||||
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: "https://web.telegram.org", forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateFocusedRect(_ rect: CGRect?) {
|
|
||||||
self.focusedRect = rect
|
|
||||||
if let (layout, navigationHeight) = self.validLayout {
|
|
||||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
||||||
self.validLayout = (layout, navigationHeight)
|
|
||||||
|
|
||||||
let sideInset: CGFloat = 66.0
|
|
||||||
let titleSpacing: CGFloat = 48.0
|
|
||||||
let bounds = CGRect(origin: CGPoint(), size: layout.size)
|
|
||||||
|
|
||||||
if case .tablet = layout.deviceMetrics.type {
|
|
||||||
if UIDevice.current.orientation == .landscapeLeft {
|
|
||||||
self.previewNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
|
||||||
} else if UIDevice.current.orientation == .landscapeRight {
|
|
||||||
self.previewNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
|
||||||
} else {
|
|
||||||
self.previewNode.transform = CATransform3DIdentity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transition.updateFrame(node: self.previewNode, frame: bounds)
|
|
||||||
transition.updateFrame(node: self.fadeNode, frame: bounds)
|
|
||||||
|
|
||||||
let frameSide = max(240.0, layout.size.width - sideInset * 2.0)
|
|
||||||
let dimHeight = ceil((layout.size.height - frameSide) / 2.0)
|
|
||||||
let dimInset = (layout.size.width - frameSide) / 2.0
|
|
||||||
|
|
||||||
let dimAlpha: CGFloat
|
|
||||||
let dimRect: CGRect
|
|
||||||
let controlsAlpha: CGFloat
|
|
||||||
let centerDimAlpha: CGFloat = 0.0
|
|
||||||
let frameAlpha: CGFloat = 1.0
|
|
||||||
if let focusedRect = self.focusedRect {
|
|
||||||
controlsAlpha = 0.0
|
|
||||||
dimAlpha = 1.0
|
|
||||||
let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6
|
|
||||||
let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height)
|
|
||||||
dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side)
|
|
||||||
} else {
|
|
||||||
controlsAlpha = 1.0
|
|
||||||
dimAlpha = 0.625
|
|
||||||
dimRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
transition.updateAlpha(node: self.topDimNode, alpha: dimAlpha)
|
|
||||||
transition.updateAlpha(node: self.bottomDimNode, alpha: dimAlpha)
|
|
||||||
transition.updateAlpha(node: self.leftDimNode, alpha: dimAlpha)
|
|
||||||
transition.updateAlpha(node: self.rightDimNode, alpha: dimAlpha)
|
|
||||||
transition.updateAlpha(node: self.centerDimNode, alpha: centerDimAlpha)
|
|
||||||
transition.updateAlpha(node: self.frameNode, alpha: frameAlpha)
|
|
||||||
|
|
||||||
transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY))
|
|
||||||
transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY)))
|
|
||||||
transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height))
|
|
||||||
transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height))
|
|
||||||
transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0))
|
|
||||||
transition.updateFrame(node: self.centerDimNode, frame: dimRect)
|
|
||||||
|
|
||||||
let buttonSize = CGSize(width: 72.0, height: 72.0)
|
|
||||||
var torchFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0), y: dimHeight + frameSide + 98.0), size: buttonSize)
|
|
||||||
let updatedTorchY = min(torchFrame.minY, layout.size.height - torchFrame.height - 10.0)
|
|
||||||
let additionalTorchOffset: CGFloat = updatedTorchY - torchFrame.minY
|
|
||||||
torchFrame.origin.y = updatedTorchY
|
|
||||||
transition.updateFrame(node: self.torchButtonNode, frame: torchFrame)
|
|
||||||
|
|
||||||
transition.updateAlpha(node: self.textNode, alpha: controlsAlpha)
|
|
||||||
transition.updateAlpha(node: self.errorTextNode, alpha: controlsAlpha)
|
|
||||||
transition.updateAlpha(node: self.torchButtonNode, alpha: controlsAlpha)
|
|
||||||
for view in self.highlightViews {
|
|
||||||
transition.updateAlpha(layer: view.layer, alpha: controlsAlpha)
|
|
||||||
}
|
|
||||||
|
|
||||||
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: layout.size.height))
|
|
||||||
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: layout.size.height))
|
|
||||||
let errorTextSize = self.errorTextNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: layout.size.height))
|
|
||||||
var textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: max(dimHeight - textSize.height - titleSpacing, navigationHeight + floorToScreenPixels((dimHeight - navigationHeight - textSize.height) / 2.0) + 5.0)), size: textSize)
|
|
||||||
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: textFrame.minY - 18.0 - titleSize.height), size: titleSize)
|
|
||||||
if titleFrame.minY < navigationHeight {
|
|
||||||
transition.updateAlpha(node: self.titleNode, alpha: 0.0)
|
|
||||||
textFrame = textFrame.offsetBy(dx: 0.0, dy: -5.0)
|
|
||||||
} else {
|
|
||||||
transition.updateAlpha(node: self.titleNode, alpha: controlsAlpha)
|
|
||||||
}
|
|
||||||
var errorTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - errorTextSize.width) / 2.0), y: dimHeight + frameSide + 48.0), size: errorTextSize)
|
|
||||||
errorTextFrame.origin.y += floor(additionalTorchOffset / 2.0)
|
|
||||||
|
|
||||||
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
|
|
||||||
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
|
||||||
transition.updateFrameAdditive(node: self.errorTextNode, frame: errorTextFrame)
|
|
||||||
|
|
||||||
if self.highlightViews.isEmpty {
|
|
||||||
let urlAttributesAndRects = self.textNode.cachedLayout?.allAttributeRects(name: "UrlAttributeT") ?? []
|
|
||||||
|
|
||||||
for (_, rect) in urlAttributesAndRects {
|
|
||||||
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
|
|
||||||
view.clipsToBounds = true
|
|
||||||
view.layer.cornerRadius = 5.0
|
|
||||||
view.frame = rect.offsetBy(dx: self.textNode.frame.minX, dy: self.textNode.frame.minY).insetBy(dx: -4.0, dy: -2.0)
|
|
||||||
self.view.insertSubview(view, belowSubview: self.textNode.view)
|
|
||||||
self.highlightViews.append(view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func torchPressed() {
|
|
||||||
self.torchButtonNode.isSelected = !self.torchButtonNode.isSelected
|
|
||||||
self.camera.setTorchActive(self.torchButtonNode.isSelected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import CoreServices
|
||||||
import Display
|
import Display
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
@ -921,7 +922,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addTextEntity() {
|
func addTextEntity() {
|
||||||
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
|
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
|
||||||
self.insertEntity.invoke(textEntity)
|
self.insertEntity.invoke(textEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -934,7 +935,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
self?.updateEntitiesPlayback.invoke(true)
|
self?.updateEntitiesPlayback.invoke(true)
|
||||||
|
|
||||||
if let file = file {
|
if let file = file {
|
||||||
let stickerEntity = DrawingStickerEntity(file: file)
|
let stickerEntity = DrawingStickerEntity(content: .file(file))
|
||||||
self?.insertEntity.invoke(stickerEntity)
|
self?.insertEntity.invoke(stickerEntity)
|
||||||
} else {
|
} else {
|
||||||
self?.updateCurrentMode(.drawing)
|
self?.updateCurrentMode(.drawing)
|
||||||
@ -1085,6 +1086,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
component: TextSettingsComponent(
|
component: TextSettingsComponent(
|
||||||
color: nil,
|
color: nil,
|
||||||
style: DrawingTextStyle(style: textEntity.style),
|
style: DrawingTextStyle(style: textEntity.style),
|
||||||
|
animation: DrawingTextAnimation(animation: textEntity.animation),
|
||||||
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
|
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
|
||||||
font: DrawingTextFont(font: textEntity.font),
|
font: DrawingTextFont(font: textEntity.font),
|
||||||
isEmojiKeyboard: false,
|
isEmojiKeyboard: false,
|
||||||
@ -1111,6 +1113,27 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
state?.updated(transition: .easeInOut(duration: 0.2))
|
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
},
|
},
|
||||||
|
toggleAnimation: { [weak state, weak textEntity] in
|
||||||
|
guard let textEntity = textEntity else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var nextAnimation: DrawingTextEntity.Animation
|
||||||
|
switch textEntity.animation {
|
||||||
|
case .none:
|
||||||
|
nextAnimation = .typing
|
||||||
|
case .typing:
|
||||||
|
nextAnimation = .wiggle
|
||||||
|
case .wiggle:
|
||||||
|
nextAnimation = .zoomIn
|
||||||
|
case .zoomIn:
|
||||||
|
nextAnimation = .none
|
||||||
|
}
|
||||||
|
textEntity.animation = nextAnimation
|
||||||
|
if let entityView = textEntity.currentEntityView {
|
||||||
|
entityView.update()
|
||||||
|
}
|
||||||
|
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
|
},
|
||||||
toggleAlignment: { [weak state, weak textEntity] in
|
toggleAlignment: { [weak state, weak textEntity] in
|
||||||
guard let textEntity = textEntity else {
|
guard let textEntity = textEntity else {
|
||||||
return
|
return
|
||||||
@ -1933,7 +1956,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, UIDropInteractionDelegate {
|
||||||
fileprivate final class Node: ViewControllerTracingNode {
|
fileprivate final class Node: ViewControllerTracingNode {
|
||||||
private weak var controller: DrawingScreen?
|
private weak var controller: DrawingScreen?
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
@ -1942,7 +1965,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
private let performAction: ActionSlot<DrawingView.Action>
|
private let performAction: ActionSlot<DrawingView.Action>
|
||||||
private let updateToolState: ActionSlot<DrawingToolState>
|
private let updateToolState: ActionSlot<DrawingToolState>
|
||||||
private let updateSelectedEntity: ActionSlot<DrawingEntity?>
|
private let updateSelectedEntity: ActionSlot<DrawingEntity?>
|
||||||
private let insertEntity: ActionSlot<DrawingEntity>
|
fileprivate let insertEntity: ActionSlot<DrawingEntity>
|
||||||
private let deselectEntity: ActionSlot<Void>
|
private let deselectEntity: ActionSlot<Void>
|
||||||
private let updateEntitiesPlayback: ActionSlot<Bool>
|
private let updateEntitiesPlayback: ActionSlot<Bool>
|
||||||
private let previewBrushSize: ActionSlot<CGFloat?>
|
private let previewBrushSize: ActionSlot<CGFloat?>
|
||||||
@ -2683,6 +2706,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
TextSettingsComponent(
|
TextSettingsComponent(
|
||||||
color: textEntity.color,
|
color: textEntity.color,
|
||||||
style: DrawingTextStyle(style: textEntity.style),
|
style: DrawingTextStyle(style: textEntity.style),
|
||||||
|
animation: DrawingTextAnimation(animation: textEntity.animation),
|
||||||
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
|
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
|
||||||
font: DrawingTextFont(font: textEntity.font),
|
font: DrawingTextFont(font: textEntity.font),
|
||||||
isEmojiKeyboard: entityView.textView.inputView != nil,
|
isEmojiKeyboard: entityView.textView.inputView != nil,
|
||||||
@ -2731,6 +2755,29 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toggleAnimation: { [weak self] in
|
||||||
|
self?.dismissFontPicker()
|
||||||
|
guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var nextAnimation: DrawingTextEntity.Animation
|
||||||
|
switch textEntity.animation {
|
||||||
|
case .none:
|
||||||
|
nextAnimation = .typing
|
||||||
|
case .typing:
|
||||||
|
nextAnimation = .wiggle
|
||||||
|
case .wiggle:
|
||||||
|
nextAnimation = .zoomIn
|
||||||
|
case .zoomIn:
|
||||||
|
nextAnimation = .none
|
||||||
|
}
|
||||||
|
textEntity.animation = nextAnimation
|
||||||
|
entityView.update()
|
||||||
|
|
||||||
|
if let (layout, orientation) = strongSelf.validLayout {
|
||||||
|
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||||
|
}
|
||||||
|
},
|
||||||
toggleAlignment: { [weak self] in
|
toggleAlignment: { [weak self] in
|
||||||
self?.dismissFontPicker()
|
self?.dismissFontPicker()
|
||||||
guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else {
|
guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else {
|
||||||
@ -2893,6 +2940,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
self.displayNode = Node(controller: self, context: self.context)
|
self.displayNode = Node(controller: self, context: self.context)
|
||||||
|
|
||||||
super.displayNodeDidLoad()
|
super.displayNodeDidLoad()
|
||||||
|
|
||||||
|
let dropInteraction = UIDropInteraction(delegate: self)
|
||||||
|
self.view.addInteraction(dropInteraction)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func generateResultData() -> TGPaintingData? {
|
public func generateResultData() -> TGPaintingData? {
|
||||||
@ -2944,15 +2994,17 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
|
|
||||||
var stickers: [Any] = []
|
var stickers: [Any] = []
|
||||||
for entity in self.entitiesView.entities {
|
for entity in self.entitiesView.entities {
|
||||||
if let sticker = entity as? DrawingStickerEntity {
|
if let sticker = entity as? DrawingStickerEntity, case let .file(file) = sticker.content {
|
||||||
let coder = PostboxEncoder()
|
let coder = PostboxEncoder()
|
||||||
coder.encodeRootObject(sticker.file)
|
coder.encodeRootObject(file)
|
||||||
stickers.append(coder.makeData())
|
stickers.append(coder.makeData())
|
||||||
} else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities {
|
} else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities {
|
||||||
for sticker in subEntities {
|
for sticker in subEntities {
|
||||||
let coder = PostboxEncoder()
|
if case let .file(file) = sticker.content {
|
||||||
coder.encodeRootObject(sticker.file)
|
let coder = PostboxEncoder()
|
||||||
stickers.append(coder.makeData())
|
coder.encodeRootObject(file)
|
||||||
|
stickers.append(coder.makeData())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3004,4 +3056,44 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
self.orientation = orientation
|
self.orientation = orientation
|
||||||
self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
|
self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||||
|
public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
|
||||||
|
return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String])
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||||
|
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
|
||||||
|
//self.chatDisplayNode.updateDropInteraction(isActive: true)
|
||||||
|
|
||||||
|
let operation: UIDropOperation
|
||||||
|
operation = .copy
|
||||||
|
return UIDropProposal(operation: operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||||
|
public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
|
||||||
|
session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let images = imageItems as! [UIImage]
|
||||||
|
|
||||||
|
//strongSelf.chatDisplayNode.updateDropInteraction(isActive: false)
|
||||||
|
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
|
||||||
|
let entity = DrawingStickerEntity(content: .image(image))
|
||||||
|
strongSelf.node.insertEntity.invoke(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||||
|
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) {
|
||||||
|
//self.chatDisplayNode.updateDropInteraction(isActive: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||||
|
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) {
|
||||||
|
//self.chatDisplayNode.updateDropInteraction(isActive: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,14 @@ import StickerResources
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
public final class DrawingStickerEntity: DrawingEntity, Codable {
|
public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||||
|
public enum Content {
|
||||||
|
case file(TelegramMediaFile)
|
||||||
|
case image(UIImage)
|
||||||
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case uuid
|
case uuid
|
||||||
case isAnimated
|
|
||||||
case file
|
case file
|
||||||
|
case image
|
||||||
case referenceDrawingSize
|
case referenceDrawingSize
|
||||||
case position
|
case position
|
||||||
case scale
|
case scale
|
||||||
@ -21,8 +25,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public let uuid: UUID
|
public let uuid: UUID
|
||||||
public let isAnimated: Bool
|
public let content: Content
|
||||||
public let file: TelegramMediaFile
|
|
||||||
|
|
||||||
public var referenceDrawingSize: CGSize
|
public var referenceDrawingSize: CGSize
|
||||||
public var position: CGPoint
|
public var position: CGPoint
|
||||||
@ -38,15 +41,22 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var baseSize: CGSize {
|
public var baseSize: CGSize {
|
||||||
let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.4)
|
let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.2)
|
||||||
return CGSize(width: size, height: size)
|
return CGSize(width: size, height: size)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(file: TelegramMediaFile) {
|
public var isAnimated: Bool {
|
||||||
|
switch self.content {
|
||||||
|
case let .file(file):
|
||||||
|
return file.isAnimatedSticker || file.isVideoSticker
|
||||||
|
case .image:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(content: Content) {
|
||||||
self.uuid = UUID()
|
self.uuid = UUID()
|
||||||
self.isAnimated = file.isAnimatedSticker || file.isVideoSticker
|
self.content = content
|
||||||
|
|
||||||
self.file = file
|
|
||||||
|
|
||||||
self.referenceDrawingSize = .zero
|
self.referenceDrawingSize = .zero
|
||||||
self.position = CGPoint()
|
self.position = CGPoint()
|
||||||
@ -58,8 +68,13 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||||
self.isAnimated = try container.decode(Bool.self, forKey: .isAnimated)
|
if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) {
|
||||||
self.file = try container.decode(TelegramMediaFile.self, forKey: .file)
|
self.content = .file(file)
|
||||||
|
} else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) {
|
||||||
|
self.content = .image(image)
|
||||||
|
} else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||||
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
||||||
@ -70,8 +85,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encode(self.uuid, forKey: .uuid)
|
try container.encode(self.uuid, forKey: .uuid)
|
||||||
try container.encode(self.isAnimated, forKey: .isAnimated)
|
switch self.content {
|
||||||
try container.encode(self.file, forKey: .file)
|
case let .file(file):
|
||||||
|
try container.encode(file, forKey: .file)
|
||||||
|
case let .image(image):
|
||||||
|
try container.encodeIfPresent(image.pngData(), forKey: .image)
|
||||||
|
}
|
||||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||||
try container.encode(self.position, forKey: .position)
|
try container.encode(self.position, forKey: .position)
|
||||||
try container.encode(self.scale, forKey: .scale)
|
try container.encode(self.scale, forKey: .scale)
|
||||||
@ -80,7 +99,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func duplicate() -> DrawingEntity {
|
public func duplicate() -> DrawingEntity {
|
||||||
let newEntity = DrawingStickerEntity(file: self.file)
|
let newEntity = DrawingStickerEntity(content: self.content)
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
newEntity.position = self.position
|
newEntity.position = self.position
|
||||||
newEntity.scale = self.scale
|
newEntity.scale = self.scale
|
||||||
@ -108,7 +127,6 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
var started: ((Double) -> Void)?
|
var started: ((Double) -> Void)?
|
||||||
|
|
||||||
private var currentSize: CGSize?
|
private var currentSize: CGSize?
|
||||||
private var dimensions: CGSize?
|
|
||||||
|
|
||||||
private let imageNode: TransformImageNode
|
private let imageNode: TransformImageNode
|
||||||
private var animationNode: AnimatedStickerNode?
|
private var animationNode: AnimatedStickerNode?
|
||||||
@ -139,46 +157,77 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
self.cachedDisposable.dispose()
|
self.cachedDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var file: TelegramMediaFile {
|
private var file: TelegramMediaFile? {
|
||||||
return (self.entity as! DrawingStickerEntity).file
|
if case let .file(file) = self.stickerEntity.content {
|
||||||
|
return file
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var image: UIImage? {
|
||||||
|
if case let .image(image) = self.stickerEntity.content {
|
||||||
|
return image
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dimensions: CGSize {
|
||||||
|
switch self.stickerEntity.content {
|
||||||
|
case let .file(file):
|
||||||
|
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||||
|
case let .image(image):
|
||||||
|
return image.size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setup() {
|
private func setup() {
|
||||||
if let dimensions = self.file.dimensions {
|
if let file = self.file {
|
||||||
if self.file.isAnimatedSticker || self.file.isVideoSticker || self.file.mimeType == "video/webm" {
|
if let dimensions = file.dimensions {
|
||||||
if self.animationNode == nil {
|
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
|
||||||
let animationNode = DefaultAnimatedStickerNodeImpl()
|
if self.animationNode == nil {
|
||||||
animationNode.autoplay = false
|
let animationNode = DefaultAnimatedStickerNodeImpl()
|
||||||
self.animationNode = animationNode
|
animationNode.autoplay = false
|
||||||
animationNode.started = { [weak self, weak animationNode] in
|
self.animationNode = animationNode
|
||||||
self?.imageNode.isHidden = true
|
animationNode.started = { [weak self, weak animationNode] in
|
||||||
|
self?.imageNode.isHidden = true
|
||||||
if let animationNode = animationNode {
|
|
||||||
let _ = (animationNode.status
|
if let animationNode = animationNode {
|
||||||
|> take(1)
|
let _ = (animationNode.status
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|> take(1)
|
||||||
self?.started?(status.duration)
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
})
|
self?.started?(status.duration)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
self.addSubnode(animationNode)
|
||||||
}
|
}
|
||||||
self.addSubnode(animationNode)
|
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0))))
|
||||||
|
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start())
|
||||||
|
} else {
|
||||||
|
if let animationNode = self.animationNode {
|
||||||
|
animationNode.visibility = false
|
||||||
|
self.animationNode = nil
|
||||||
|
animationNode.removeFromSupernode()
|
||||||
|
self.imageNode.isHidden = false
|
||||||
|
self.didSetUpAnimationNode = false
|
||||||
|
}
|
||||||
|
self.imageNode.setSignal(chatMessageSticker(account: self.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false))
|
||||||
|
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start())
|
||||||
}
|
}
|
||||||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
self.setNeedsLayout()
|
||||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: self.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0))))
|
|
||||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(self.file), resource: self.file.resource).start())
|
|
||||||
} else {
|
|
||||||
if let animationNode = self.animationNode {
|
|
||||||
animationNode.visibility = false
|
|
||||||
self.animationNode = nil
|
|
||||||
animationNode.removeFromSupernode()
|
|
||||||
self.imageNode.isHidden = false
|
|
||||||
self.didSetUpAnimationNode = false
|
|
||||||
}
|
|
||||||
self.imageNode.setSignal(chatMessageSticker(account: self.context.account, userLocation: .other, file: self.file, small: false, synchronousLoad: false))
|
|
||||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(self.file), resource: chatMessageStickerResource(file: self.file, small: false)).start())
|
|
||||||
}
|
}
|
||||||
|
} else if let image = self.image {
|
||||||
self.dimensions = dimensions.cgSize
|
self.imageNode.setSignal(.single({ arguments -> DrawingContext? in
|
||||||
|
let context = DrawingContext(size: arguments.drawingSize, opaque: false, clear: true)
|
||||||
|
context?.withFlippedContext({ ctx in
|
||||||
|
if let cgImage = image.cgImage {
|
||||||
|
ctx.draw(cgImage, in: CGRect(origin: .zero, size: arguments.drawingSize))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return context
|
||||||
|
}))
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,15 +264,17 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
if self.isPlaying != isPlaying {
|
if self.isPlaying != isPlaying {
|
||||||
self.isPlaying = isPlaying
|
self.isPlaying = isPlaying
|
||||||
|
|
||||||
if isPlaying && !self.didSetUpAnimationNode {
|
if let file = self.file {
|
||||||
self.didSetUpAnimationNode = true
|
if isPlaying && !self.didSetUpAnimationNode {
|
||||||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
self.didSetUpAnimationNode = true
|
||||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker || self.file.mimeType == "video/webm")
|
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||||
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
|
||||||
|
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||||
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|
|
||||||
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|
||||||
|
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.animationNode?.visibility = isPlaying
|
self.animationNode?.visibility = isPlaying
|
||||||
}
|
}
|
||||||
@ -241,33 +292,29 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
let sideSize: CGFloat = size.width
|
let sideSize: CGFloat = size.width
|
||||||
let boundingSize = CGSize(width: sideSize, height: sideSize)
|
let boundingSize = CGSize(width: sideSize, height: sideSize)
|
||||||
|
|
||||||
if let dimensions = self.dimensions {
|
|
||||||
let imageSize = dimensions.aspectFitted(boundingSize)
|
let imageSize = self.dimensions.aspectFitted(boundingSize)
|
||||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||||
if let animationNode = self.animationNode {
|
if let animationNode = self.animationNode {
|
||||||
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||||
animationNode.updateLayout(size: imageSize)
|
animationNode.updateLayout(size: imageSize)
|
||||||
|
|
||||||
if !self.didApplyVisibility {
|
if !self.didApplyVisibility {
|
||||||
self.didApplyVisibility = true
|
self.didApplyVisibility = true
|
||||||
self.applyVisibility()
|
self.applyVisibility()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.update(animated: false)
|
|
||||||
}
|
}
|
||||||
|
self.update(animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func update(animated: Bool) {
|
override func update(animated: Bool) {
|
||||||
guard let dimensions = self.stickerEntity.file.dimensions?.cgSize else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.center = self.stickerEntity.position
|
self.center = self.stickerEntity.position
|
||||||
|
|
||||||
let size = self.stickerEntity.baseSize
|
let size = self.stickerEntity.baseSize
|
||||||
|
|
||||||
self.bounds = CGRect(origin: .zero, size: dimensions.aspectFitted(size))
|
self.bounds = CGRect(origin: .zero, size: self.dimensions.aspectFitted(size))
|
||||||
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale)
|
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale)
|
||||||
|
|
||||||
var transform = CATransform3DIdentity
|
var transform = CATransform3DIdentity
|
||||||
@ -297,13 +344,13 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
self.pushIdentityTransformForMeasurement()
|
self.pushIdentityTransformForMeasurement()
|
||||||
|
|
||||||
selectionView.transform = .identity
|
selectionView.transform = .identity
|
||||||
let bounds = self.selectionBounds
|
let maxSide = max(self.selectionBounds.width, self.selectionBounds.height)
|
||||||
let center = bounds.center
|
let center = self.selectionBounds.center
|
||||||
|
|
||||||
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||||
selectionView.center = self.convert(center, to: selectionView.superview)
|
selectionView.center = self.convert(center, to: selectionView.superview)
|
||||||
|
|
||||||
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0))
|
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (maxSide * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (maxSide * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0))
|
||||||
selectionView.transform = CGAffineTransformMakeRotation(self.stickerEntity.rotation)
|
selectionView.transform = CGAffineTransformMakeRotation(self.stickerEntity.rotation)
|
||||||
|
|
||||||
self.popIdentityTransformForMeasurement()
|
self.popIdentityTransformForMeasurement()
|
||||||
|
@ -43,6 +43,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
case text
|
case text
|
||||||
case textAttributes
|
case textAttributes
|
||||||
case style
|
case style
|
||||||
|
case animation
|
||||||
case font
|
case font
|
||||||
case alignment
|
case alignment
|
||||||
case fontSize
|
case fontSize
|
||||||
@ -61,19 +62,13 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
case filled
|
case filled
|
||||||
case semi
|
case semi
|
||||||
case stroke
|
case stroke
|
||||||
|
}
|
||||||
init(style: DrawingTextEntity.Style) {
|
|
||||||
switch style {
|
enum Animation: Codable {
|
||||||
case .regular:
|
case none
|
||||||
self = .regular
|
case typing
|
||||||
case .filled:
|
case wiggle
|
||||||
self = .filled
|
case zoomIn
|
||||||
case .semi:
|
|
||||||
self = .semi
|
|
||||||
case .stroke:
|
|
||||||
self = .stroke
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Font: Codable {
|
enum Font: Codable {
|
||||||
@ -100,6 +95,9 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
|
|
||||||
public var uuid: UUID
|
public var uuid: UUID
|
||||||
public var isAnimated: Bool {
|
public var isAnimated: Bool {
|
||||||
|
if self.animation != .none {
|
||||||
|
return true
|
||||||
|
}
|
||||||
var isAnimated = false
|
var isAnimated = false
|
||||||
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
|
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
|
||||||
if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||||
@ -111,6 +109,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
|
|
||||||
var text: NSAttributedString
|
var text: NSAttributedString
|
||||||
var style: Style
|
var style: Style
|
||||||
|
var animation: Animation
|
||||||
var font: Font
|
var font: Font
|
||||||
var alignment: Alignment
|
var alignment: Alignment
|
||||||
var fontSize: CGFloat
|
var fontSize: CGFloat
|
||||||
@ -130,11 +129,12 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
public var renderImage: UIImage?
|
public var renderImage: UIImage?
|
||||||
public var renderSubEntities: [DrawingStickerEntity]?
|
public var renderSubEntities: [DrawingStickerEntity]?
|
||||||
|
|
||||||
init(text: NSAttributedString, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) {
|
init(text: NSAttributedString, style: Style, animation: Animation, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) {
|
||||||
self.uuid = UUID()
|
self.uuid = UUID()
|
||||||
|
|
||||||
self.text = text
|
self.text = text
|
||||||
self.style = style
|
self.style = style
|
||||||
|
self.animation = animation
|
||||||
self.font = font
|
self.font = font
|
||||||
self.alignment = alignment
|
self.alignment = alignment
|
||||||
self.fontSize = fontSize
|
self.fontSize = fontSize
|
||||||
@ -160,6 +160,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
self.text = attributedString
|
self.text = attributedString
|
||||||
|
|
||||||
self.style = try container.decode(Style.self, forKey: .style)
|
self.style = try container.decode(Style.self, forKey: .style)
|
||||||
|
self.animation = try container.decode(Animation.self, forKey: .animation)
|
||||||
self.font = try container.decode(Font.self, forKey: .font)
|
self.font = try container.decode(Font.self, forKey: .font)
|
||||||
self.alignment = try container.decode(Alignment.self, forKey: .alignment)
|
self.alignment = try container.decode(Alignment.self, forKey: .alignment)
|
||||||
self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize)
|
self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize)
|
||||||
@ -191,6 +192,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
try container.encode(textAttributes, forKey: .textAttributes)
|
try container.encode(textAttributes, forKey: .textAttributes)
|
||||||
|
|
||||||
try container.encode(self.style, forKey: .style)
|
try container.encode(self.style, forKey: .style)
|
||||||
|
try container.encode(self.animation, forKey: .animation)
|
||||||
try container.encode(self.font, forKey: .font)
|
try container.encode(self.font, forKey: .font)
|
||||||
try container.encode(self.alignment, forKey: .alignment)
|
try container.encode(self.alignment, forKey: .alignment)
|
||||||
try container.encode(self.fontSize, forKey: .fontSize)
|
try container.encode(self.fontSize, forKey: .fontSize)
|
||||||
@ -210,7 +212,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func duplicate() -> DrawingEntity {
|
public func duplicate() -> DrawingEntity {
|
||||||
let newEntity = DrawingTextEntity(text: self.text, style: self.style, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color)
|
let newEntity = DrawingTextEntity(text: self.text, style: self.style, animation: self.animation, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color)
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
newEntity.position = self.position
|
newEntity.position = self.position
|
||||||
newEntity.width = self.width
|
newEntity.width = self.width
|
||||||
@ -700,7 +702,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||||||
}
|
}
|
||||||
let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0)
|
let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0)
|
||||||
|
|
||||||
let entity = DrawingStickerEntity(file: file)
|
let entity = DrawingStickerEntity(content: .file(file))
|
||||||
entity.referenceDrawingSize = CGSize(width: itemSize * 2.5, height: itemSize * 2.5)
|
entity.referenceDrawingSize = CGSize(width: itemSize * 2.5, height: itemSize * 2.5)
|
||||||
entity.scale = scale
|
entity.scale = scale
|
||||||
entity.position = textPosition.offsetBy(
|
entity.position = textPosition.offsetBy(
|
||||||
|
@ -27,6 +27,26 @@ enum DrawingTextStyle: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DrawingTextAnimation: Equatable {
|
||||||
|
case none
|
||||||
|
case typing
|
||||||
|
case wiggle
|
||||||
|
case zoomIn
|
||||||
|
|
||||||
|
init(animation: DrawingTextEntity.Animation) {
|
||||||
|
switch animation {
|
||||||
|
case .none:
|
||||||
|
self = .none
|
||||||
|
case .typing:
|
||||||
|
self = .typing
|
||||||
|
case .wiggle:
|
||||||
|
self = .wiggle
|
||||||
|
case .zoomIn:
|
||||||
|
self = .zoomIn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum DrawingTextAlignment: Equatable {
|
enum DrawingTextAlignment: Equatable {
|
||||||
case left
|
case left
|
||||||
case center
|
case center
|
||||||
@ -266,6 +286,7 @@ final class TextFontComponent: Component {
|
|||||||
final class TextSettingsComponent: CombinedComponent {
|
final class TextSettingsComponent: CombinedComponent {
|
||||||
let color: DrawingColor?
|
let color: DrawingColor?
|
||||||
let style: DrawingTextStyle
|
let style: DrawingTextStyle
|
||||||
|
let animation: DrawingTextAnimation
|
||||||
let alignment: DrawingTextAlignment
|
let alignment: DrawingTextAlignment
|
||||||
let font: DrawingTextFont
|
let font: DrawingTextFont
|
||||||
let isEmojiKeyboard: Bool
|
let isEmojiKeyboard: Bool
|
||||||
@ -277,6 +298,7 @@ final class TextSettingsComponent: CombinedComponent {
|
|||||||
let updateFastColorPickerPan: (CGPoint) -> Void
|
let updateFastColorPickerPan: (CGPoint) -> Void
|
||||||
let dismissFastColorPicker: () -> Void
|
let dismissFastColorPicker: () -> Void
|
||||||
let toggleStyle: () -> Void
|
let toggleStyle: () -> Void
|
||||||
|
let toggleAnimation: () -> Void
|
||||||
let toggleAlignment: () -> Void
|
let toggleAlignment: () -> Void
|
||||||
let presentFontPicker: () -> Void
|
let presentFontPicker: () -> Void
|
||||||
let toggleKeyboard: (() -> Void)?
|
let toggleKeyboard: (() -> Void)?
|
||||||
@ -284,6 +306,7 @@ final class TextSettingsComponent: CombinedComponent {
|
|||||||
init(
|
init(
|
||||||
color: DrawingColor?,
|
color: DrawingColor?,
|
||||||
style: DrawingTextStyle,
|
style: DrawingTextStyle,
|
||||||
|
animation: DrawingTextAnimation,
|
||||||
alignment: DrawingTextAlignment,
|
alignment: DrawingTextAlignment,
|
||||||
font: DrawingTextFont,
|
font: DrawingTextFont,
|
||||||
isEmojiKeyboard: Bool,
|
isEmojiKeyboard: Bool,
|
||||||
@ -294,12 +317,14 @@ final class TextSettingsComponent: CombinedComponent {
|
|||||||
updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in },
|
updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in },
|
||||||
dismissFastColorPicker: @escaping () -> Void = {},
|
dismissFastColorPicker: @escaping () -> Void = {},
|
||||||
toggleStyle: @escaping () -> Void,
|
toggleStyle: @escaping () -> Void,
|
||||||
|
toggleAnimation: @escaping () -> Void,
|
||||||
toggleAlignment: @escaping () -> Void,
|
toggleAlignment: @escaping () -> Void,
|
||||||
presentFontPicker: @escaping () -> Void,
|
presentFontPicker: @escaping () -> Void,
|
||||||
toggleKeyboard: (() -> Void)?
|
toggleKeyboard: (() -> Void)?
|
||||||
) {
|
) {
|
||||||
self.color = color
|
self.color = color
|
||||||
self.style = style
|
self.style = style
|
||||||
|
self.animation = animation
|
||||||
self.alignment = alignment
|
self.alignment = alignment
|
||||||
self.font = font
|
self.font = font
|
||||||
self.isEmojiKeyboard = isEmojiKeyboard
|
self.isEmojiKeyboard = isEmojiKeyboard
|
||||||
@ -310,6 +335,7 @@ final class TextSettingsComponent: CombinedComponent {
|
|||||||
self.updateFastColorPickerPan = updateFastColorPickerPan
|
self.updateFastColorPickerPan = updateFastColorPickerPan
|
||||||
self.dismissFastColorPicker = dismissFastColorPicker
|
self.dismissFastColorPicker = dismissFastColorPicker
|
||||||
self.toggleStyle = toggleStyle
|
self.toggleStyle = toggleStyle
|
||||||
|
self.toggleAnimation = toggleAnimation
|
||||||
self.toggleAlignment = toggleAlignment
|
self.toggleAlignment = toggleAlignment
|
||||||
self.presentFontPicker = presentFontPicker
|
self.presentFontPicker = presentFontPicker
|
||||||
self.toggleKeyboard = toggleKeyboard
|
self.toggleKeyboard = toggleKeyboard
|
||||||
@ -322,6 +348,9 @@ final class TextSettingsComponent: CombinedComponent {
|
|||||||
if lhs.style != rhs.style {
|
if lhs.style != rhs.style {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.animation != rhs.animation {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.alignment != rhs.alignment {
|
if lhs.alignment != rhs.alignment {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let account: Account
|
let account: Account
|
||||||
let file: TelegramMediaFile
|
let file: TelegramMediaFile?
|
||||||
let entity: DrawingStickerEntity
|
let entity: DrawingStickerEntity
|
||||||
let animated: Bool
|
let animated: Bool
|
||||||
let durationPromise = Promise<Double>()
|
let durationPromise = Promise<Double>()
|
||||||
@ -95,47 +95,53 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity {
|
|||||||
init(account: Account, entity: DrawingStickerEntity) {
|
init(account: Account, entity: DrawingStickerEntity) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
self.file = entity.file
|
self.animated = entity.isAnimated
|
||||||
self.animated = file.isAnimatedSticker || file.isVideoSticker
|
|
||||||
|
switch entity.content {
|
||||||
if file.isAnimatedSticker || file.isVideoSticker {
|
case let .file(file):
|
||||||
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker)
|
self.file = file
|
||||||
if let source = self.source {
|
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
|
||||||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker)
|
||||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384))
|
if let source = self.source {
|
||||||
self.disposables.add((source.cachedDataPath(width: Int(fittedDimensions.width), height: Int(fittedDimensions.height))
|
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
|
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384))
|
||||||
|
self.disposables.add((source.cachedDataPath(width: Int(fittedDimensions.width), height: Int(fittedDimensions.height))
|
||||||
|> deliverOn(self.queue)).start(next: { [weak self] path, complete in
|
|> deliverOn(self.queue)).start(next: { [weak self] path, complete in
|
||||||
if let strongSelf = self, complete {
|
if let strongSelf = self, complete {
|
||||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||||
let queue = strongSelf.queue
|
let queue = strongSelf.queue
|
||||||
let frameSource = AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})!
|
let frameSource = AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})!
|
||||||
strongSelf.frameCount = frameSource.frameCount
|
strongSelf.frameCount = frameSource.frameCount
|
||||||
strongSelf.frameRate = frameSource.frameRate
|
strongSelf.frameRate = frameSource.frameRate
|
||||||
|
|
||||||
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
|
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
|
||||||
strongSelf.totalDuration = duration
|
strongSelf.totalDuration = duration
|
||||||
|
|
||||||
strongSelf.durationPromise.set(.single(duration))
|
strongSelf.durationPromise.set(.single(duration))
|
||||||
|
|
||||||
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
|
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
|
||||||
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
|
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
|
||||||
})
|
})
|
||||||
strongSelf.frameQueue.set(.single(frameQueue))
|
strongSelf.frameQueue.set(.single(frameQueue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.disposables.add((chatMessageSticker(account: self.account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false)
|
||||||
|
|> deliverOn(self.queue)).start(next: { [weak self] generator in
|
||||||
|
if let strongSelf = self {
|
||||||
|
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: entity.baseSize, boundingSize: entity.baseSize, intrinsicInsets: UIEdgeInsets()))
|
||||||
|
let image = context?.generateImage()
|
||||||
|
if let image = image {
|
||||||
|
strongSelf.imagePromise.set(.single(image))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} else {
|
case let .image(image):
|
||||||
self.disposables.add((chatMessageSticker(account: self.account, userLocation: .other, file: self.file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false)
|
self.file = nil
|
||||||
|> deliverOn(self.queue)).start(next: { [weak self] generator in
|
self.imagePromise.set(.single(image))
|
||||||
if let strongSelf = self {
|
|
||||||
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: entity.baseSize, boundingSize: entity.baseSize, intrinsicInsets: UIEdgeInsets()))
|
|
||||||
let image = context?.generateImage()
|
|
||||||
if let image = image {
|
|
||||||
strongSelf.imagePromise.set(.single(image))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,6 +291,99 @@ public final class QrCodeScanScreen: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class FrameNode: ASDisplayNode {
|
||||||
|
let topLeftLine: CAShapeLayer
|
||||||
|
let topRightLine: CAShapeLayer
|
||||||
|
let bottomLeftLine: CAShapeLayer
|
||||||
|
let bottomRightLine: CAShapeLayer
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.topLeftLine = CAShapeLayer()
|
||||||
|
self.topRightLine = CAShapeLayer()
|
||||||
|
self.bottomLeftLine = CAShapeLayer()
|
||||||
|
self.bottomRightLine = CAShapeLayer()
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
for line in self.lines {
|
||||||
|
line.strokeColor = UIColor.white.cgColor
|
||||||
|
line.fillColor = UIColor.clear.cgColor
|
||||||
|
line.lineWidth = 4.0
|
||||||
|
line.lineCap = .round
|
||||||
|
self.layer.addSublayer(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lines: [CAShapeLayer] {
|
||||||
|
return [
|
||||||
|
self.topLeftLine,
|
||||||
|
self.topRightLine,
|
||||||
|
self.bottomLeftLine,
|
||||||
|
self.bottomRightLine
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn() {
|
||||||
|
let strokeStart = self.topLeftLine.strokeStart
|
||||||
|
let strokeEnd = self.topLeftLine.strokeEnd
|
||||||
|
|
||||||
|
let duration: Double = 0.85
|
||||||
|
let delay: Double = 0.15
|
||||||
|
|
||||||
|
for line in self.lines {
|
||||||
|
line.animateSpring(from: 0.0 as NSNumber, to: strokeStart as NSNumber, keyPath: "strokeStart", duration: duration, delay: delay)
|
||||||
|
line.animateSpring(from: 1.0 as NSNumber, to: strokeEnd as NSNumber, keyPath: "strokeEnd", duration: duration, delay: delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(size: CGSize) {
|
||||||
|
let cornerRadius: CGFloat = 6.0
|
||||||
|
|
||||||
|
let lineLength = size.width / 2.0 - cornerRadius
|
||||||
|
let targetLineLength = 24.0
|
||||||
|
let fraction = targetLineLength / lineLength
|
||||||
|
let strokeFraction = (1.0 - fraction) / 2.0
|
||||||
|
let strokeStart = strokeFraction
|
||||||
|
let strokeEnd = 1.0 - strokeFraction
|
||||||
|
|
||||||
|
let topLeftPath = CGMutablePath()
|
||||||
|
topLeftPath.move(to: CGPoint(x: 0.0, y: size.height / 2.0))
|
||||||
|
topLeftPath.addArc(center: CGPoint(x: cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: -.pi, endAngle: -.pi / 2.0, clockwise: false)
|
||||||
|
topLeftPath.addLine(to: CGPoint(x: size.width / 2.0, y: 0.0))
|
||||||
|
self.topLeftLine.path = topLeftPath
|
||||||
|
self.topLeftLine.strokeStart = strokeStart
|
||||||
|
self.topLeftLine.strokeEnd = strokeEnd
|
||||||
|
|
||||||
|
let topRightPath = CGMutablePath()
|
||||||
|
topRightPath.move(to: CGPoint(x: size.width / 2.0, y: 0.0))
|
||||||
|
topRightPath.addArc(center: CGPoint(x: size.width - cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: -.pi / 2.0, endAngle: 0.0, clockwise: false)
|
||||||
|
topRightPath.addLine(to: CGPoint(x: size.width, y: size.height / 2.0))
|
||||||
|
self.topRightLine.path = topRightPath
|
||||||
|
self.topRightLine.strokeStart = strokeStart
|
||||||
|
self.topRightLine.strokeEnd = strokeEnd
|
||||||
|
|
||||||
|
let bottomRightPath = CGMutablePath()
|
||||||
|
bottomRightPath.move(to: CGPoint(x: size.width, y: size.height / 2.0))
|
||||||
|
bottomRightPath.addArc(center: CGPoint(x: size.width - cornerRadius, y: size.height - cornerRadius), radius: cornerRadius, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: false)
|
||||||
|
bottomRightPath.addLine(to: CGPoint(x: size.width / 2.0, y: size.height))
|
||||||
|
self.bottomRightLine.path = bottomRightPath
|
||||||
|
self.bottomRightLine.strokeStart = strokeStart
|
||||||
|
self.bottomRightLine.strokeEnd = strokeEnd
|
||||||
|
|
||||||
|
let bottomLeftPath = CGMutablePath()
|
||||||
|
bottomLeftPath.move(to: CGPoint(x: size.width / 2.0, y: size.height))
|
||||||
|
bottomLeftPath.addArc(center: CGPoint(x: cornerRadius, y: size.height - cornerRadius), radius: cornerRadius, startAngle: .pi / 2.0, endAngle: .pi, clockwise: false)
|
||||||
|
bottomLeftPath.addLine(to: CGPoint(x: 0.0, y: size.height / 2.0))
|
||||||
|
self.bottomLeftLine.path = bottomLeftPath
|
||||||
|
self.bottomLeftLine.strokeStart = strokeStart
|
||||||
|
self.bottomLeftLine.strokeEnd = strokeEnd
|
||||||
|
|
||||||
|
for line in self.lines {
|
||||||
|
line.frame = CGRect(origin: .zero, size: size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollViewDelegate {
|
private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
@ -304,7 +397,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
|
|||||||
private let leftDimNode: ASDisplayNode
|
private let leftDimNode: ASDisplayNode
|
||||||
private let rightDimNode: ASDisplayNode
|
private let rightDimNode: ASDisplayNode
|
||||||
private let centerDimNode: ASDisplayNode
|
private let centerDimNode: ASDisplayNode
|
||||||
private let frameNode: ASImageNode
|
private let frameNode: FrameNode
|
||||||
private let galleryButtonNode: GlassButtonNode
|
private let galleryButtonNode: GlassButtonNode
|
||||||
private let torchButtonNode: GlassButtonNode
|
private let torchButtonNode: GlassButtonNode
|
||||||
private let titleNode: ImmediateTextNode
|
private let titleNode: ImmediateTextNode
|
||||||
@ -350,28 +443,29 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
|
|||||||
self.fadeNode.alpha = 0.0
|
self.fadeNode.alpha = 0.0
|
||||||
self.fadeNode.backgroundColor = .black
|
self.fadeNode.backgroundColor = .black
|
||||||
|
|
||||||
|
let dimColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
||||||
|
|
||||||
self.topDimNode = ASDisplayNode()
|
self.topDimNode = ASDisplayNode()
|
||||||
self.topDimNode.alpha = 0.625
|
self.topDimNode.alpha = 0.625
|
||||||
self.topDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
self.topDimNode.backgroundColor = dimColor
|
||||||
|
|
||||||
self.bottomDimNode = ASDisplayNode()
|
self.bottomDimNode = ASDisplayNode()
|
||||||
self.bottomDimNode.alpha = 0.625
|
self.bottomDimNode.alpha = 0.625
|
||||||
self.bottomDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
self.bottomDimNode.backgroundColor = dimColor
|
||||||
|
|
||||||
self.leftDimNode = ASDisplayNode()
|
self.leftDimNode = ASDisplayNode()
|
||||||
self.leftDimNode.alpha = 0.625
|
self.leftDimNode.alpha = 0.625
|
||||||
self.leftDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
self.leftDimNode.backgroundColor = dimColor
|
||||||
|
|
||||||
self.rightDimNode = ASDisplayNode()
|
self.rightDimNode = ASDisplayNode()
|
||||||
self.rightDimNode.alpha = 0.625
|
self.rightDimNode.alpha = 0.625
|
||||||
self.rightDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
self.rightDimNode.backgroundColor = dimColor
|
||||||
|
|
||||||
self.centerDimNode = ASDisplayNode()
|
self.centerDimNode = ASDisplayNode()
|
||||||
self.centerDimNode.alpha = 0.0
|
self.centerDimNode.alpha = 0.0
|
||||||
self.centerDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
|
self.centerDimNode.backgroundColor = dimColor
|
||||||
|
|
||||||
self.frameNode = ASImageNode()
|
self.frameNode = FrameNode()
|
||||||
self.frameNode.image = generateFrameImage()
|
|
||||||
|
|
||||||
self.galleryButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraGalleryIcon")!, label: nil)
|
self.galleryButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraGalleryIcon")!, label: nil)
|
||||||
self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil)
|
self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil)
|
||||||
@ -517,6 +611,16 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
|
|||||||
self.textNode.view.addGestureRecognizer(recognizer)
|
self.textNode.view.addGestureRecognizer(recognizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var animatedIn = false
|
||||||
|
func animateIn() {
|
||||||
|
guard !self.animatedIn else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.animatedIn = true
|
||||||
|
|
||||||
|
self.frameNode.animateIn()
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||||
switch recognizer.state {
|
switch recognizer.state {
|
||||||
case .ended:
|
case .ended:
|
||||||
@ -551,9 +655,16 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
private var animatingIn = false
|
||||||
|
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, animateIn: Bool = false, transition: ContainedViewLayoutTransition) {
|
||||||
|
let hadLayout = self.validLayout != nil
|
||||||
self.validLayout = (layout, navigationHeight)
|
self.validLayout = (layout, navigationHeight)
|
||||||
|
|
||||||
|
var prepareForAnimateIn = false
|
||||||
|
if !hadLayout {
|
||||||
|
prepareForAnimateIn = true
|
||||||
|
}
|
||||||
|
|
||||||
let sideInset: CGFloat = 66.0
|
let sideInset: CGFloat = 66.0
|
||||||
let titleSpacing: CGFloat = 48.0
|
let titleSpacing: CGFloat = 48.0
|
||||||
let bounds = CGRect(origin: CGPoint(), size: layout.size)
|
let bounds = CGRect(origin: CGPoint(), size: layout.size)
|
||||||
@ -571,11 +682,20 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
|
|||||||
transition.updateFrame(node: self.fadeNode, frame: bounds)
|
transition.updateFrame(node: self.fadeNode, frame: bounds)
|
||||||
|
|
||||||
let frameSide = max(240.0, layout.size.width - sideInset * 2.0)
|
let frameSide = max(240.0, layout.size.width - sideInset * 2.0)
|
||||||
|
let animateInScale: CGFloat = 0.4
|
||||||
|
var effectiveFrameSide = frameSide
|
||||||
|
if prepareForAnimateIn {
|
||||||
|
effectiveFrameSide = round(effectiveFrameSide * animateInScale)
|
||||||
|
}
|
||||||
|
|
||||||
let dimHeight = ceil((layout.size.height - frameSide) / 2.0)
|
let dimHeight = ceil((layout.size.height - frameSide) / 2.0)
|
||||||
|
let effectiveDimHeight = ceil((layout.size.height - effectiveFrameSide) / 2.0)
|
||||||
let dimInset = (layout.size.width - frameSide) / 2.0
|
let dimInset = (layout.size.width - frameSide) / 2.0
|
||||||
|
let effectiveDimInset = (layout.size.width - effectiveFrameSide) / 2.0
|
||||||
|
|
||||||
let dimAlpha: CGFloat
|
let dimAlpha: CGFloat
|
||||||
let dimRect: CGRect
|
let dimRect: CGRect
|
||||||
|
let frameRect: CGRect
|
||||||
let controlsAlpha: CGFloat
|
let controlsAlpha: CGFloat
|
||||||
let centerDimAlpha: CGFloat = 0.0
|
let centerDimAlpha: CGFloat = 0.0
|
||||||
let frameAlpha: CGFloat = 1.0
|
let frameAlpha: CGFloat = 1.0
|
||||||
@ -585,10 +705,12 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
|
|||||||
let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6
|
let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6
|
||||||
let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height)
|
let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height)
|
||||||
dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side)
|
dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side)
|
||||||
|
frameRect = dimRect
|
||||||
} else {
|
} else {
|
||||||
controlsAlpha = 1.0
|
controlsAlpha = 1.0
|
||||||
dimAlpha = 0.625
|
dimAlpha = 0.625
|
||||||
dimRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0)
|
dimRect = CGRect(x: effectiveDimInset, y: effectiveDimHeight, width: layout.size.width - effectiveDimInset * 2.0, height: layout.size.height - effectiveDimHeight * 2.0)
|
||||||
|
frameRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.updateAlpha(node: self.topDimNode, alpha: dimAlpha)
|
transition.updateAlpha(node: self.topDimNode, alpha: dimAlpha)
|
||||||
@ -598,12 +720,25 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
|
|||||||
transition.updateAlpha(node: self.centerDimNode, alpha: centerDimAlpha)
|
transition.updateAlpha(node: self.centerDimNode, alpha: centerDimAlpha)
|
||||||
transition.updateAlpha(node: self.frameNode, alpha: frameAlpha)
|
transition.updateAlpha(node: self.frameNode, alpha: frameAlpha)
|
||||||
|
|
||||||
transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY))
|
if !self.animatingIn {
|
||||||
transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY)))
|
var delay: Double = 0.0
|
||||||
transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height))
|
if animateIn {
|
||||||
transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height))
|
self.animatingIn = true
|
||||||
transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0))
|
delay = 0.1
|
||||||
transition.updateFrame(node: self.centerDimNode, frame: dimRect)
|
}
|
||||||
|
transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY), delay: delay, completion: { _ in
|
||||||
|
self.animatingIn = false
|
||||||
|
})
|
||||||
|
transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY)), delay: delay)
|
||||||
|
transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height), delay: delay)
|
||||||
|
transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height), delay: delay)
|
||||||
|
transition.updateFrame(node: self.frameNode, frame: frameRect)
|
||||||
|
self.frameNode.updateLayout(size: frameRect.size)
|
||||||
|
transition.updateFrame(node: self.centerDimNode, frame: frameRect)
|
||||||
|
if animateIn {
|
||||||
|
transition.animateTransformScale(node: self.frameNode, from: CGPoint(x: animateInScale, y: animateInScale), delay: delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let buttonSize = CGSize(width: 72.0, height: 72.0)
|
let buttonSize = CGSize(width: 72.0, height: 72.0)
|
||||||
var torchFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0), y: dimHeight + frameSide + 98.0), size: buttonSize)
|
var torchFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0), y: dimHeight + frameSide + 98.0), size: buttonSize)
|
||||||
@ -657,6 +792,11 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
|
|||||||
self.highlightViews.append(view)
|
self.highlightViews.append(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prepareForAnimateIn {
|
||||||
|
self.animateIn()
|
||||||
|
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, animateIn: true, transition: .animated(duration: 0.8, curve: .customSpring(damping: 88.0, initialVelocity: 0.0)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func galleryPressed() {
|
@objc private func galleryPressed() {
|
||||||
|
@ -88,7 +88,6 @@ swift_library(
|
|||||||
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
|
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
|
||||||
"//submodules/OpenInExternalAppUI:OpenInExternalAppUI",
|
"//submodules/OpenInExternalAppUI:OpenInExternalAppUI",
|
||||||
"//submodules/AccountUtils:AccountUtils",
|
"//submodules/AccountUtils:AccountUtils",
|
||||||
"//submodules/AuthTransferUI:AuthTransferUI",
|
|
||||||
"//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils",
|
"//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils",
|
||||||
"//submodules/DebugSettingsUI:DebugSettingsUI",
|
"//submodules/DebugSettingsUI:DebugSettingsUI",
|
||||||
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
||||||
|
@ -9,7 +9,6 @@ import TelegramUIPreferences
|
|||||||
import ItemListUI
|
import ItemListUI
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import AuthTransferUI
|
|
||||||
import ItemListPeerActionItem
|
import ItemListPeerActionItem
|
||||||
import DeviceAccess
|
import DeviceAccess
|
||||||
import QrCodeUI
|
import QrCodeUI
|
||||||
|
@ -9,7 +9,6 @@ import TelegramUIPreferences
|
|||||||
import ItemListUI
|
import ItemListUI
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import AuthTransferUI
|
|
||||||
import ItemListPeerActionItem
|
import ItemListPeerActionItem
|
||||||
import DeviceAccess
|
import DeviceAccess
|
||||||
import QrCodeUI
|
import QrCodeUI
|
||||||
|
@ -268,7 +268,6 @@ swift_library(
|
|||||||
"//submodules/Svg:Svg",
|
"//submodules/Svg:Svg",
|
||||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||||
"//submodules/TooltipUI:TooltipUI",
|
"//submodules/TooltipUI:TooltipUI",
|
||||||
"//submodules/AuthTransferUI:AuthTransferUI",
|
|
||||||
"//submodules/ListMessageItem:ListMessageItem",
|
"//submodules/ListMessageItem:ListMessageItem",
|
||||||
"//submodules/FileMediaResourceStatus:FileMediaResourceStatus",
|
"//submodules/FileMediaResourceStatus:FileMediaResourceStatus",
|
||||||
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
|
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
|
||||||
|
@ -9668,7 +9668,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
let peerId = self.chatLocation.peerId
|
let peerId = self.chatLocation.peerId
|
||||||
if let subject = self.subject, case .scheduledMessages = subject {
|
if let subject = self.subject, case .scheduledMessages = subject {
|
||||||
} else {
|
} else {
|
||||||
self.buttonUnreadCountDisposable = (self.context.chatLocationUnreadCount(for: self.chatLocation, contextHolder: self.chatLocationContextHolder)
|
let throttledUnreadCountSignal = self.context.chatLocationUnreadCount(for: self.chatLocation, contextHolder: self.chatLocationContextHolder)
|
||||||
|
|> mapToThrottled { value -> Signal<Int, NoError> in
|
||||||
|
return .single(value) |> then(.complete() |> delay(0.2, queue: Queue.mainQueue()))
|
||||||
|
}
|
||||||
|
self.buttonUnreadCountDisposable = (throttledUnreadCountSignal
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] count in
|
|> deliverOnMainQueue).start(next: { [weak self] count in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
|
@ -6,7 +6,7 @@ import TelegramPresentationData
|
|||||||
import WallpaperBackgroundNode
|
import WallpaperBackgroundNode
|
||||||
import AnimatedCountLabelNode
|
import AnimatedCountLabelNode
|
||||||
|
|
||||||
private let badgeFont = Font.regular(13.0)
|
private let badgeFont = Font.with(size: 13.0, traits: [.monospacedNumbers])
|
||||||
|
|
||||||
enum ChatHistoryNavigationButtonType {
|
enum ChatHistoryNavigationButtonType {
|
||||||
case down
|
case down
|
||||||
@ -197,7 +197,7 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
self.badgeBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
self.badgeBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
} else if previousValue != self.currentValue {
|
} else if previousValue < self.currentValue {
|
||||||
self.badgeBackgroundNode.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
|
self.badgeBackgroundNode.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.badgeBackgroundNode.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false, completion: { _ in
|
strongSelf.badgeBackgroundNode.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false, completion: { _ in
|
||||||
|
@ -42,7 +42,6 @@ import ChatListUI
|
|||||||
import CallListUI
|
import CallListUI
|
||||||
import AccountUtils
|
import AccountUtils
|
||||||
import PassportUI
|
import PassportUI
|
||||||
import AuthTransferUI
|
|
||||||
import DeviceAccess
|
import DeviceAccess
|
||||||
import LegacyMediaPickerUI
|
import LegacyMediaPickerUI
|
||||||
import TelegramNotices
|
import TelegramNotices
|
||||||
|
Loading…
x
Reference in New Issue
Block a user