mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 14:17:19 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
319 lines
11 KiB
Swift
319 lines
11 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import GlassBackgroundComponent
|
|
|
|
private let regularTitleFont = Font.regular(36.0)
|
|
private let regularSubtitleFont: UIFont = {
|
|
return UIFont.systemFont(ofSize: 10.0, weight: UIFont.Weight.bold)
|
|
}()
|
|
|
|
private let largeTitleFont = Font.regular(40.0)
|
|
private let largeSubtitleFont: UIFont = {
|
|
return UIFont.systemFont(ofSize: 12.0, weight: UIFont.Weight.bold)
|
|
}()
|
|
|
|
private func generateButtonContentImage(size: CGSize, title: String, subtitle: String) -> UIImage? {
|
|
return generateImage(size, contextGenerator: { size, context in
|
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
|
context.clear(bounds)
|
|
|
|
context.beginPath()
|
|
context.addEllipse(in: bounds)
|
|
context.clip()
|
|
|
|
context.textMatrix = .identity
|
|
|
|
let titleFont: UIFont
|
|
let subtitleFont: UIFont
|
|
let titleOffset: CGFloat
|
|
let subtitleOffset: CGFloat
|
|
if size.width > 80.0 {
|
|
titleFont = largeTitleFont
|
|
subtitleFont = largeSubtitleFont
|
|
if subtitle.isEmpty {
|
|
titleOffset = -18.0
|
|
} else {
|
|
titleOffset = -11.0
|
|
}
|
|
subtitleOffset = -54.0
|
|
} else if size.width > 70.0 {
|
|
titleFont = regularTitleFont
|
|
subtitleFont = regularSubtitleFont
|
|
if subtitle.isEmpty {
|
|
titleOffset = -17.0
|
|
} else {
|
|
titleOffset = -10.0
|
|
}
|
|
subtitleOffset = -48.0
|
|
}
|
|
else {
|
|
titleFont = regularTitleFont
|
|
subtitleFont = regularSubtitleFont
|
|
if subtitle.isEmpty {
|
|
titleOffset = -11.0
|
|
} else {
|
|
titleOffset = -4.0
|
|
}
|
|
subtitleOffset = -41.0
|
|
}
|
|
|
|
let titlePath = CGMutablePath()
|
|
titlePath.addRect(bounds.offsetBy(dx: 0.0, dy: titleOffset))
|
|
let titleString = NSAttributedString(string: title, font: titleFont, textColor: .white, paragraphAlignment: .center)
|
|
let titleFramesetter = CTFramesetterCreateWithAttributedString(titleString as CFAttributedString)
|
|
let titleFrame = CTFramesetterCreateFrame(titleFramesetter, CFRangeMake(0, titleString.length), titlePath, nil)
|
|
CTFrameDraw(titleFrame, context)
|
|
|
|
if !subtitle.isEmpty {
|
|
let subtitlePath = CGMutablePath()
|
|
subtitlePath.addRect(bounds.offsetBy(dx: 0.0, dy: subtitleOffset))
|
|
let subtitleString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: .white, paragraphAlignment: .center)
|
|
let subtitleFramesetter = CTFramesetterCreateWithAttributedString(subtitleString as CFAttributedString)
|
|
let subtitleFrame = CTFramesetterCreateFrame(subtitleFramesetter, CFRangeMake(0, subtitleString.length), subtitlePath, nil)
|
|
CTFrameDraw(subtitleFrame, context)
|
|
}
|
|
})
|
|
}
|
|
|
|
final class PasscodeEntryButtonNode: ASDisplayNode {
|
|
private var presentationData: PresentationData
|
|
let title: String
|
|
private let subtitle: String
|
|
|
|
private let backgroundView: GlassBackgroundView
|
|
private let button: HighlightTrackingButton
|
|
private let contentNode: ASImageNode
|
|
|
|
var action: (() -> Void)?
|
|
var cancelAction: (() -> Void)?
|
|
|
|
init(presentationData: PresentationData, title: String, subtitle: String) {
|
|
self.presentationData = presentationData
|
|
self.title = title
|
|
self.subtitle = subtitle
|
|
|
|
self.backgroundView = GlassBackgroundView()
|
|
|
|
self.button = HighlightTrackingButton()
|
|
self.button.accessibilityLabel = title
|
|
self.button.accessibilityTraits = .keyboardKey
|
|
|
|
self.contentNode = ASImageNode()
|
|
self.contentNode.displaysAsynchronously = false
|
|
self.contentNode.displayWithoutProcessing = true
|
|
self.contentNode.isUserInteractionEnabled = false
|
|
|
|
super.init()
|
|
|
|
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchDown)
|
|
self.button.addTarget(self, action: #selector(self.buttonCancelled), for: [.touchUpOutside, .touchCancel])
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.view.addSubview(self.backgroundView)
|
|
self.backgroundView.contentView.addSubview(self.button)
|
|
self.button.addSubview(self.contentNode.view)
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
self.action?()
|
|
}
|
|
|
|
@objc private func buttonCancelled() {
|
|
self.cancelAction?()
|
|
}
|
|
|
|
override var frame: CGRect {
|
|
get {
|
|
return super.frame
|
|
}
|
|
set {
|
|
super.frame = newValue
|
|
self.updateGraphics()
|
|
}
|
|
}
|
|
|
|
func updatePresentationData(_ presentationData: PresentationData) {
|
|
self.presentationData = presentationData
|
|
self.updateGraphics()
|
|
self.setNeedsLayout()
|
|
}
|
|
|
|
private func updateGraphics() {
|
|
self.contentNode.image = generateButtonContentImage(size: self.bounds.size, title: self.title, subtitle: self.subtitle)
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
|
|
self.backgroundView.frame = self.bounds
|
|
self.backgroundView.update(size: self.bounds.size, cornerRadius: self.bounds.height / 2.0, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .clear), isInteractive: true, transition: .immediate)
|
|
self.button.frame = self.bounds
|
|
self.contentNode.frame = self.bounds
|
|
}
|
|
}
|
|
|
|
private let buttonsData = [
|
|
("1", " "),
|
|
("2", "A B C"),
|
|
("3", "D E F"),
|
|
("4", "G H I"),
|
|
("5", "J K L"),
|
|
("6", "M N O"),
|
|
("7", "P Q R S"),
|
|
("8", "T U V"),
|
|
("9", "W X Y Z"),
|
|
("0", "")
|
|
]
|
|
|
|
final class PasscodeEntryKeyboardNode: ASDisplayNode {
|
|
private var presentationData: PresentationData?
|
|
private var background: PasscodeBackground?
|
|
|
|
private let backgroundContainer = GlassBackgroundContainerView()
|
|
private var buttonNodes: [PasscodeEntryButtonNode] = []
|
|
|
|
var charactedEntered: ((String) -> Void)?
|
|
var backspace: (() -> Void)?
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.view.addSubview(self.backgroundContainer)
|
|
for buttonNode in self.buttonNodes {
|
|
self.backgroundContainer.contentView.addSubview(buttonNode.view)
|
|
}
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
|
|
self.backgroundContainer.frame = self.bounds
|
|
self.backgroundContainer.update(size: self.bounds.size, isDark: self.presentationData?.theme.overallDarkAppearance ?? false, transition: .immediate)
|
|
}
|
|
|
|
private func updateButtons() {
|
|
guard let presentationData = self.presentationData, self.background != nil else {
|
|
return
|
|
}
|
|
|
|
if !self.buttonNodes.isEmpty {
|
|
for button in self.buttonNodes {
|
|
button.updatePresentationData(presentationData)
|
|
}
|
|
} else {
|
|
for (title, subtitle) in buttonsData {
|
|
let buttonNode = PasscodeEntryButtonNode(presentationData: presentationData, title: title, subtitle: subtitle)
|
|
buttonNode.action = { [weak self] in
|
|
self?.charactedEntered?(title)
|
|
}
|
|
buttonNode.cancelAction = { [weak self] in
|
|
self?.backspace?()
|
|
}
|
|
self.buttonNodes.append(buttonNode)
|
|
if self.isNodeLoaded {
|
|
self.backgroundContainer.contentView.addSubview(buttonNode.view)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateBackground(_ presentationData: PresentationData, _ background: PasscodeBackground) {
|
|
self.presentationData = presentationData
|
|
self.background = background
|
|
self.updateButtons()
|
|
self.setNeedsLayout()
|
|
}
|
|
|
|
func animateIn() {
|
|
for (i, buttonNode) in self.buttonNodes.enumerated() {
|
|
var delay: Double = 0.001
|
|
if i / 3 == 1 {
|
|
delay = 0.05
|
|
}
|
|
else if i / 3 == 2 {
|
|
delay = 0.1
|
|
}
|
|
else if i / 3 == 3 {
|
|
delay = 0.15
|
|
}
|
|
buttonNode.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.25, delay: delay, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
|
}
|
|
}
|
|
|
|
func updateLayout(layout: PasscodeLayout, transition: ContainedViewLayoutTransition) -> (CGRect, CGSize) {
|
|
let origin: CGPoint
|
|
let buttonSize: CGFloat
|
|
let horizontalSecond: CGFloat
|
|
let horizontalThird: CGFloat
|
|
let verticalSecond: CGFloat
|
|
let verticalThird: CGFloat
|
|
let verticalFourth: CGFloat
|
|
let keyboardSize: CGSize
|
|
|
|
if layout.layout.orientation == .landscape && layout.layout.deviceMetrics.type != .tablet {
|
|
let horizontalSpacing: CGFloat = 20.0
|
|
let verticalSpacing: CGFloat = 12.0
|
|
buttonSize = 65.0
|
|
keyboardSize = CGSize(width: buttonSize * 3.0 + horizontalSpacing * 2.0, height: buttonSize * 4.0 + verticalSpacing * 3.0)
|
|
horizontalSecond = buttonSize + horizontalSpacing
|
|
horizontalThird = buttonSize * 2.0 + horizontalSpacing * 2.0
|
|
verticalSecond = buttonSize + verticalSpacing
|
|
verticalThird = buttonSize * 2.0 + verticalSpacing * 2.0
|
|
verticalFourth = buttonSize * 3.0 + verticalSpacing * 3.0
|
|
origin = CGPoint(x: floor(layout.layout.size.width / 2.0 + (layout.layout.size.width / 2.0 - keyboardSize.width) / 2.0) - layout.layout.safeInsets.right, y: floor((layout.layout.size.height - keyboardSize.height) / 2.0))
|
|
} else {
|
|
origin = CGPoint(x: floor((layout.layout.size.width - layout.keyboard.size.width) / 2.0), y: layout.keyboard.topOffset)
|
|
buttonSize = layout.keyboard.buttonSize
|
|
horizontalSecond = layout.keyboard.horizontalSecond
|
|
horizontalThird = layout.keyboard.horizontalThird
|
|
verticalSecond = layout.keyboard.verticalSecond
|
|
verticalThird = layout.keyboard.verticalThird
|
|
verticalFourth = layout.keyboard.verticalFourth
|
|
keyboardSize = layout.keyboard.size
|
|
}
|
|
|
|
for (i, buttonNode) in self.buttonNodes.enumerated() {
|
|
var origin = origin
|
|
if i % 3 == 0 {
|
|
origin.x += 0.0
|
|
} else if (i % 3 == 1) {
|
|
origin.x += horizontalSecond
|
|
}
|
|
else {
|
|
origin.x += horizontalThird
|
|
}
|
|
|
|
if i / 3 == 0 {
|
|
origin.y += 0.0
|
|
}
|
|
else if i / 3 == 1 {
|
|
origin.y += verticalSecond
|
|
}
|
|
else if i / 3 == 2 {
|
|
origin.y += verticalThird
|
|
}
|
|
else if i / 3 == 3 {
|
|
origin.x += horizontalSecond
|
|
origin.y += verticalFourth
|
|
}
|
|
transition.updateFrame(node: buttonNode, frame: CGRect(origin: origin, size: CGSize(width: buttonSize, height: buttonSize)))
|
|
}
|
|
return (CGRect(origin: origin, size: keyboardSize), CGSize(width: buttonSize, height: buttonSize))
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
if let result = result, result.isDescendant(of: self.view) {
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
}
|