mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Cherry-pick various fixes
This commit is contained in:
parent
702f254783
commit
000b1c7339
@ -7365,3 +7365,38 @@ Sorry for the inconvenience.";
|
|||||||
"CreateExternalStream.StreamKey" = "stream key";
|
"CreateExternalStream.StreamKey" = "stream key";
|
||||||
"CreateExternalStream.StartStreamingInfo" = "Once you start broadcasting in your streaming\napp, tap Start Streaming below.";
|
"CreateExternalStream.StartStreamingInfo" = "Once you start broadcasting in your streaming\napp, tap Start Streaming below.";
|
||||||
"CreateExternalStream.StartStreaming" = "Start Streaming";
|
"CreateExternalStream.StartStreaming" = "Start Streaming";
|
||||||
|
|
||||||
|
"Translate.Title" = "Translate";
|
||||||
|
"Translate.CopyTranslation" = "Copy Translation";
|
||||||
|
"Translate.ChangeLanguage" = "Change Language";
|
||||||
|
"Translate.More" = "more";
|
||||||
|
"Translate.Languages.Title" = "Languages";
|
||||||
|
"Translate.Languages.Original" = "Original";
|
||||||
|
"Translate.Languages.Translation" = "Translation";
|
||||||
|
|
||||||
|
"Bot.AddToChat" = "Add to Group or Channel";
|
||||||
|
"Bot.AddToChatInfo" = "This bot is able to manage a group or channel.";
|
||||||
|
|
||||||
|
"Bot.AddToChat.Title" = "Add to Group or Channel";
|
||||||
|
"Bot.AddToChat.MyChannels" = "CHANNEL I MANAGE";
|
||||||
|
"Bot.AddToChat.MyGroups" = "GROUPS I MANAGE";
|
||||||
|
"Bot.AddToChat.OtherGroups" = "GROUPS";
|
||||||
|
|
||||||
|
"Bot.AddToChat.Add.Title" = "Add Bot";
|
||||||
|
"Bot.AddToChat.Add.AdminRights" = "Admin Rights";
|
||||||
|
"Bot.AddToChat.Add.AddAsAdmin" = "Add Bot as Admin";
|
||||||
|
"Bot.AddToChat.Add.AddAsMember" = "Add Bot as Member";
|
||||||
|
|
||||||
|
"Bot.AddToChat.Add.AdminAlertTitle" = "Add Bot as Admin?";
|
||||||
|
"Bot.AddToChat.Add.AdminAlertTextGroup" = "Are you sure you want to add the bot as an admin in the group **%@**?";
|
||||||
|
"Bot.AddToChat.Add.AdminAlertTextChannel" = "Are you sure you want to add the bot as an admin in the channel **%@**?";
|
||||||
|
"Bot.AddToChat.Add.AdminAlertAdd" = "Add as Admin";
|
||||||
|
|
||||||
|
"Bot.AddToChat.Add.MemberAlertTitle" = "Add Bot as Member?";
|
||||||
|
"Bot.AddToChat.Add.MemberAlertTextGroup" = "Are you sure you want to add the bot as a member in the group **%@**?";
|
||||||
|
"Bot.AddToChat.Add.MemberAlertTextChannel" = "Are you sure you want to add the bot as a member in the channel **%@**?";
|
||||||
|
"Bot.AddToChat.Add.MemberAlertAdd" = "Add as Member";
|
||||||
|
|
||||||
|
"PeerInfo.ButtonStop" = "Stop";
|
||||||
|
|
||||||
|
"Localization.ShowTranslateInfoExtended" = "Show 'Translate' button in the message context menu.\n\nGoogle may have access to the messages you translate.";
|
||||||
|
@ -1143,7 +1143,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string
|
text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string
|
||||||
return (current, inputMode)
|
return (current, inputMode)
|
||||||
}
|
}
|
||||||
speakText(text)
|
let _ = speakText(text)
|
||||||
|
|
||||||
if #available(iOS 13.0, *) {
|
if #available(iOS 13.0, *) {
|
||||||
UIMenuController.shared.hideMenu()
|
UIMenuController.shared.hideMenu()
|
||||||
|
@ -255,7 +255,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
let topInset: CGFloat = edgeTopInset
|
let topInset: CGFloat = edgeTopInset
|
||||||
|
|
||||||
var dismissing = false
|
var dismissing = false
|
||||||
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 600.0) {
|
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) {
|
||||||
self.interactivelyDismissed?()
|
self.interactivelyDismissed?()
|
||||||
dismissing = true
|
dismissing = true
|
||||||
} else if self.isExpanded {
|
} else if self.isExpanded {
|
||||||
|
@ -279,6 +279,9 @@ public class AttachmentController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
guard !self.isDismissing else {
|
||||||
|
return
|
||||||
|
}
|
||||||
if case .ended = recognizer.state {
|
if case .ended = recognizer.state {
|
||||||
if let controller = self.currentControllers.last {
|
if let controller = self.currentControllers.last {
|
||||||
controller.requestDismiss(completion: { [weak self] in
|
controller.requestDismiss(completion: { [weak self] in
|
||||||
@ -436,9 +439,9 @@ public class AttachmentController: ViewController {
|
|||||||
self.animating = true
|
self.animating = true
|
||||||
if case .regular = layout.metrics.widthClass {
|
if case .regular = layout.metrics.widthClass {
|
||||||
self.layer.allowsGroupOpacity = true
|
self.layer.allowsGroupOpacity = true
|
||||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
let _ = self.container.dismiss(transition: .immediate, completion: completion)
|
let _ = self?.container.dismiss(transition: .immediate, completion: completion)
|
||||||
self.animating = false
|
self?.animating = false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||||
@ -515,7 +518,6 @@ public class AttachmentController: ViewController {
|
|||||||
self.wrapperNode.view.mask = nil
|
self.wrapperNode.view.mask = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting)
|
let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting)
|
||||||
let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, transition: transition)
|
let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, transition: transition)
|
||||||
var panelTransition = transition
|
var panelTransition = transition
|
||||||
@ -583,6 +585,10 @@ public class AttachmentController: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
print()
|
||||||
|
}
|
||||||
|
|
||||||
public required init(coder aDecoder: NSCoder) {
|
public required init(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
@ -596,11 +602,15 @@ public class AttachmentController: ViewController {
|
|||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func _dismiss() {
|
||||||
|
super.dismiss(animated: false, completion: {})
|
||||||
|
}
|
||||||
|
|
||||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||||
self.view.endEditing(true)
|
self.view.endEditing(true)
|
||||||
if flag {
|
if flag {
|
||||||
self.node.animateOut(completion: {
|
self.node.animateOut(completion: { [weak self] in
|
||||||
super.dismiss(animated: false, completion: {})
|
self?._dismiss()
|
||||||
completion?()
|
completion?()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -275,7 +275,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
strongSelf.editableControlNode = editableControlNode
|
strongSelf.editableControlNode = editableControlNode
|
||||||
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode)
|
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.containerNode)
|
||||||
editableControlNode.frame = editableControlFrame
|
editableControlNode.frame = editableControlFrame
|
||||||
transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY))
|
transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY))
|
||||||
editableControlNode.alpha = 0.0
|
editableControlNode.alpha = 0.0
|
||||||
|
@ -5,6 +5,7 @@ public final class Button: Component {
|
|||||||
public let content: AnyComponent<Empty>
|
public let content: AnyComponent<Empty>
|
||||||
public let minSize: CGSize?
|
public let minSize: CGSize?
|
||||||
public let tag: AnyObject?
|
public let tag: AnyObject?
|
||||||
|
public let automaticHighlight: Bool
|
||||||
public let action: () -> Void
|
public let action: () -> Void
|
||||||
|
|
||||||
convenience public init(
|
convenience public init(
|
||||||
@ -15,6 +16,7 @@ public final class Button: Component {
|
|||||||
content: content,
|
content: content,
|
||||||
minSize: nil,
|
minSize: nil,
|
||||||
tag: nil,
|
tag: nil,
|
||||||
|
automaticHighlight: true,
|
||||||
action: action
|
action: action
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -23,11 +25,13 @@ public final class Button: Component {
|
|||||||
content: AnyComponent<Empty>,
|
content: AnyComponent<Empty>,
|
||||||
minSize: CGSize?,
|
minSize: CGSize?,
|
||||||
tag: AnyObject? = nil,
|
tag: AnyObject? = nil,
|
||||||
|
automaticHighlight: Bool = true,
|
||||||
action: @escaping () -> Void
|
action: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.content = content
|
self.content = content
|
||||||
self.minSize = nil
|
self.minSize = nil
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
|
self.automaticHighlight = automaticHighlight
|
||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,6 +40,7 @@ public final class Button: Component {
|
|||||||
content: self.content,
|
content: self.content,
|
||||||
minSize: minSize,
|
minSize: minSize,
|
||||||
tag: self.tag,
|
tag: self.tag,
|
||||||
|
automaticHighlight: self.automaticHighlight,
|
||||||
action: self.action
|
action: self.action
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -45,6 +50,7 @@ public final class Button: Component {
|
|||||||
content: self.content,
|
content: self.content,
|
||||||
minSize: self.minSize,
|
minSize: self.minSize,
|
||||||
tag: tag,
|
tag: tag,
|
||||||
|
automaticHighlight: self.automaticHighlight,
|
||||||
action: self.action
|
action: self.action
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -59,6 +65,9 @@ public final class Button: Component {
|
|||||||
if lhs.tag !== rhs.tag {
|
if lhs.tag !== rhs.tag {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.automaticHighlight != rhs.automaticHighlight {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +77,9 @@ public final class Button: Component {
|
|||||||
private var component: Button?
|
private var component: Button?
|
||||||
private var currentIsHighlighted: Bool = false {
|
private var currentIsHighlighted: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
|
guard let component = self.component, component.automaticHighlight else {
|
||||||
|
return
|
||||||
|
}
|
||||||
if self.currentIsHighlighted != oldValue {
|
if self.currentIsHighlighted != oldValue {
|
||||||
self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0
|
self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0
|
||||||
}
|
}
|
||||||
|
@ -2,26 +2,31 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public final class Circle: Component {
|
public final class Circle: Component {
|
||||||
public let color: UIColor
|
public let fillColor: UIColor
|
||||||
|
public let strokeColor: UIColor
|
||||||
|
public let strokeWidth: CGFloat
|
||||||
public let size: CGSize
|
public let size: CGSize
|
||||||
public let width: CGFloat
|
|
||||||
|
|
||||||
public init(color: UIColor, size: CGSize, width: CGFloat) {
|
public init(fillColor: UIColor = .clear, strokeColor: UIColor = .clear, strokeWidth: CGFloat = 0.0, size: CGSize) {
|
||||||
self.color = color
|
self.fillColor = fillColor
|
||||||
|
self.strokeColor = strokeColor
|
||||||
|
self.strokeWidth = strokeWidth
|
||||||
self.size = size
|
self.size = size
|
||||||
self.width = width
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: Circle, rhs: Circle) -> Bool {
|
public static func ==(lhs: Circle, rhs: Circle) -> Bool {
|
||||||
if !lhs.color.isEqual(rhs.color) {
|
if !lhs.fillColor.isEqual(rhs.fillColor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !lhs.strokeColor.isEqual(rhs.strokeColor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strokeWidth != rhs.strokeWidth {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.size != rhs.size {
|
if lhs.size != rhs.size {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.width != rhs.width {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,9 +43,13 @@ public final class Circle: Component {
|
|||||||
|
|
||||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||||
if let context = UIGraphicsGetCurrentContext() {
|
if let context = UIGraphicsGetCurrentContext() {
|
||||||
context.setStrokeColor(component.color.cgColor)
|
context.setFillColor(component.fillColor.cgColor)
|
||||||
context.setLineWidth(component.width)
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: component.width / 2.0, dy: component.width / 2.0))
|
if component.strokeWidth > 0.0 {
|
||||||
|
context.setStrokeColor(component.strokeColor.cgColor)
|
||||||
|
context.setLineWidth(component.strokeWidth)
|
||||||
|
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: component.strokeWidth / 2.0, dy: component.strokeWidth / 2.0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.image = UIGraphicsGetImageFromCurrentImageContext()
|
self.image = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public extension Gesture {
|
||||||
|
enum LongPressGestureState {
|
||||||
|
case began
|
||||||
|
case ended
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class LongPressGesture: Gesture {
|
||||||
|
private class Impl: UILongPressGestureRecognizer {
|
||||||
|
var action: (LongPressGestureState) -> Void
|
||||||
|
|
||||||
|
init(pressDuration: Double, action: @escaping (LongPressGestureState) -> Void) {
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
super.init(target: nil, action: nil)
|
||||||
|
self.minimumPressDuration = pressDuration
|
||||||
|
self.addTarget(self, action: #selector(self.onAction))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func onAction() {
|
||||||
|
switch self.state {
|
||||||
|
case .began:
|
||||||
|
self.action(.began)
|
||||||
|
case .ended, .cancelled:
|
||||||
|
self.action(.ended)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static let id = Id()
|
||||||
|
|
||||||
|
private let pressDuration: Double
|
||||||
|
private let action: (LongPressGestureState) -> Void
|
||||||
|
|
||||||
|
init(pressDuration: Double, action: @escaping (LongPressGestureState) -> Void) {
|
||||||
|
self.pressDuration = pressDuration
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
super.init(id: Self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func create() -> UIGestureRecognizer {
|
||||||
|
return Impl(pressDuration: self.pressDuration, action: self.action)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func update(gesture: UIGestureRecognizer) {
|
||||||
|
(gesture as! Impl).minimumPressDuration = self.pressDuration
|
||||||
|
(gesture as! Impl).action = self.action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func longPress(duration: Double = 0.2, _ action: @escaping (LongPressGestureState) -> Void) -> Gesture {
|
||||||
|
return LongPressGesture(pressDuration: duration, action: action)
|
||||||
|
}
|
||||||
|
}
|
@ -32,8 +32,8 @@ public final class ComponentHostView<EnvironmentType: Equatable>: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(transition: Transition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, containerSize: CGSize) -> CGSize {
|
public func update(transition: Transition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize {
|
||||||
let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: false, containerSize: containerSize)
|
let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: forceUpdate, containerSize: containerSize)
|
||||||
self.currentSize = size
|
self.currentSize = size
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ public final class MultilineTextComponent: Component {
|
|||||||
public var truncationType: CTLineTruncationType
|
public var truncationType: CTLineTruncationType
|
||||||
public var maximumNumberOfLines: Int
|
public var maximumNumberOfLines: Int
|
||||||
public var lineSpacing: CGFloat
|
public var lineSpacing: CGFloat
|
||||||
|
public var cutout: TextNodeCutout?
|
||||||
public var insets: UIEdgeInsets
|
public var insets: UIEdgeInsets
|
||||||
public var textShadowColor: UIColor?
|
public var textShadowColor: UIColor?
|
||||||
public var textStroke: (UIColor, CGFloat)?
|
public var textStroke: (UIColor, CGFloat)?
|
||||||
@ -21,6 +22,7 @@ public final class MultilineTextComponent: Component {
|
|||||||
truncationType: CTLineTruncationType = .end,
|
truncationType: CTLineTruncationType = .end,
|
||||||
maximumNumberOfLines: Int = 1,
|
maximumNumberOfLines: Int = 1,
|
||||||
lineSpacing: CGFloat = 0.0,
|
lineSpacing: CGFloat = 0.0,
|
||||||
|
cutout: TextNodeCutout? = nil,
|
||||||
insets: UIEdgeInsets = UIEdgeInsets(),
|
insets: UIEdgeInsets = UIEdgeInsets(),
|
||||||
textShadowColor: UIColor? = nil,
|
textShadowColor: UIColor? = nil,
|
||||||
textStroke: (UIColor, CGFloat)? = nil
|
textStroke: (UIColor, CGFloat)? = nil
|
||||||
@ -31,6 +33,7 @@ public final class MultilineTextComponent: Component {
|
|||||||
self.truncationType = truncationType
|
self.truncationType = truncationType
|
||||||
self.maximumNumberOfLines = maximumNumberOfLines
|
self.maximumNumberOfLines = maximumNumberOfLines
|
||||||
self.lineSpacing = lineSpacing
|
self.lineSpacing = lineSpacing
|
||||||
|
self.cutout = cutout
|
||||||
self.insets = insets
|
self.insets = insets
|
||||||
self.textShadowColor = textShadowColor
|
self.textShadowColor = textShadowColor
|
||||||
self.textStroke = textStroke
|
self.textStroke = textStroke
|
||||||
@ -55,6 +58,9 @@ public final class MultilineTextComponent: Component {
|
|||||||
if lhs.lineSpacing != rhs.lineSpacing {
|
if lhs.lineSpacing != rhs.lineSpacing {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.cutout != rhs.cutout {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.insets != rhs.insets {
|
if lhs.insets != rhs.insets {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -93,7 +99,7 @@ public final class MultilineTextComponent: Component {
|
|||||||
alignment: component.horizontalAlignment,
|
alignment: component.horizontalAlignment,
|
||||||
verticalAlignment: component.verticalAlignment,
|
verticalAlignment: component.verticalAlignment,
|
||||||
lineSpacing: component.lineSpacing,
|
lineSpacing: component.lineSpacing,
|
||||||
cutout: nil,
|
cutout: component.cutout,
|
||||||
insets: component.insets,
|
insets: component.insets,
|
||||||
textShadowColor: component.textShadowColor,
|
textShadowColor: component.textShadowColor,
|
||||||
textStroke: component.textStroke,
|
textStroke: component.textStroke,
|
||||||
|
@ -21,6 +21,15 @@ public extension Transition.Animation.Curve {
|
|||||||
self = .spring
|
self = .spring
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve {
|
||||||
|
switch self {
|
||||||
|
case .easeInOut:
|
||||||
|
return .easeInOut
|
||||||
|
case .spring:
|
||||||
|
return .spring
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Transition {
|
public extension Transition {
|
||||||
@ -32,6 +41,15 @@ public extension Transition {
|
|||||||
self.init(animation: .curve(duration: duration, curve: Transition.Animation.Curve(curve)))
|
self.init(animation: .curve(duration: duration, curve: Transition.Animation.Curve(curve)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var containedViewLayoutTransition: ContainedViewLayoutTransition {
|
||||||
|
switch self.animation {
|
||||||
|
case .none:
|
||||||
|
return .immediate
|
||||||
|
case let .curve(duration, curve):
|
||||||
|
return .animated(duration: duration, curve: curve.containedViewLayoutTransitionCurve)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class ViewControllerComponentContainer: ViewController {
|
open class ViewControllerComponentContainer: ViewController {
|
||||||
@ -125,7 +143,7 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
let environment = ViewControllerComponentContainer.Environment(
|
let environment = ViewControllerComponentContainer.Environment(
|
||||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||||
navigationHeight: navigationHeight,
|
navigationHeight: navigationHeight,
|
||||||
safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.intrinsicInsets.left + layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.intrinsicInsets.right + layout.safeInsets.right),
|
safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right),
|
||||||
isVisible: self.currentIsVisible,
|
isVisible: self.currentIsVisible,
|
||||||
theme: self.presentationData.theme,
|
theme: self.presentationData.theme,
|
||||||
strings: self.presentationData.strings,
|
strings: self.presentationData.strings,
|
||||||
|
@ -848,6 +848,11 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self._keepModalDismissProgress {
|
||||||
|
modalStyleOverlayTransitionFactor = 0.0
|
||||||
|
self._keepModalDismissProgress = false
|
||||||
|
}
|
||||||
|
|
||||||
topModalDismissProgress = max(topModalDismissProgress, modalStyleOverlayTransitionFactor)
|
topModalDismissProgress = max(topModalDismissProgress, modalStyleOverlayTransitionFactor)
|
||||||
|
|
||||||
switch layout.metrics.widthClass {
|
switch layout.metrics.widthClass {
|
||||||
@ -1358,6 +1363,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
self._viewControllersPromise.set(self.viewControllers)
|
self._viewControllersPromise.set(self.viewControllers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var _keepModalDismissProgress = false
|
||||||
public func presentOverlay(controller: ViewController, inGlobal: Bool = false, blockInteraction: Bool = false) {
|
public func presentOverlay(controller: ViewController, inGlobal: Bool = false, blockInteraction: Bool = false) {
|
||||||
let container = NavigationOverlayContainer(controller: controller, blocksInteractionUntilReady: blockInteraction, controllerRemoved: { [weak self] controller in
|
let container = NavigationOverlayContainer(controller: controller, blocksInteractionUntilReady: blockInteraction, controllerRemoved: { [weak self] controller in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -1385,6 +1391,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.updateContainersNonReentrant(transition: .immediate)
|
strongSelf.updateContainersNonReentrant(transition: .immediate)
|
||||||
}, statusBarUpdated: { [weak self] transition in
|
}, statusBarUpdated: { [weak self] transition in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
|
@ -39,7 +39,7 @@ swift_library(
|
|||||||
"//submodules/Speak:Speak",
|
"//submodules/Speak:Speak",
|
||||||
"//submodules/UndoUI:UndoUI",
|
"//submodules/UndoUI:UndoUI",
|
||||||
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
|
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
|
||||||
"//submodules/Translate:Translate",
|
"//submodules/TranslateUI:TranslateUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -16,7 +16,7 @@ import PresentationDataUtils
|
|||||||
import ImageContentAnalysis
|
import ImageContentAnalysis
|
||||||
import TextSelectionNode
|
import TextSelectionNode
|
||||||
import Speak
|
import Speak
|
||||||
import Translate
|
import TranslateUI
|
||||||
import ShareController
|
import ShareController
|
||||||
import UndoUI
|
import UndoUI
|
||||||
|
|
||||||
@ -352,9 +352,12 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
window.rootViewController?.present(controller, animated: true)
|
window.rootViewController?.present(controller, animated: true)
|
||||||
}
|
}
|
||||||
case .speak:
|
case .speak:
|
||||||
speakText(string)
|
let _ = speakText(string)
|
||||||
case .translate:
|
case .translate:
|
||||||
translateText(context: strongSelf.context, text: string)
|
if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController {
|
||||||
|
let controller = TranslateScreen(context: strongSelf.context, text: string, fromLanguage: nil)
|
||||||
|
parentController.present(controller, in: .window(.root))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
recognizedContentNode.barcodeAction = { [weak self] payload, rect in
|
recognizedContentNode.barcodeAction = { [weak self] payload, rect in
|
||||||
|
@ -25,7 +25,7 @@ swift_library(
|
|||||||
"//submodules/AppBundle:AppBundle",
|
"//submodules/AppBundle:AppBundle",
|
||||||
"//submodules/LocationResources:LocationResources",
|
"//submodules/LocationResources:LocationResources",
|
||||||
"//submodules/UndoUI:UndoUI",
|
"//submodules/UndoUI:UndoUI",
|
||||||
"//submodules/Translate:Translate",
|
"//submodules/TranslateUI:TranslateUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -16,7 +16,7 @@ import OpenInExternalAppUI
|
|||||||
import LocationUI
|
import LocationUI
|
||||||
import UndoUI
|
import UndoUI
|
||||||
import ContextUI
|
import ContextUI
|
||||||
import Translate
|
import TranslateUI
|
||||||
|
|
||||||
final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||||
private weak var controller: InstantPageController?
|
private weak var controller: InstantPageController?
|
||||||
@ -1048,8 +1048,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
|
let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
|
||||||
if canTranslate {
|
if canTranslate {
|
||||||
actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: {
|
actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { [weak self] in
|
||||||
translateText(context: context, text: text, fromLang: language)
|
let controller = TranslateScreen(context: context, text: text, fromLanguage: language)
|
||||||
|
self?.present(controller, nil)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,8 +21,6 @@ import SparseItemGrid
|
|||||||
import UndoUI
|
import UndoUI
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
|
|
||||||
let overflowInset: CGFloat = 0.0
|
|
||||||
|
|
||||||
final class MediaPickerInteraction {
|
final class MediaPickerInteraction {
|
||||||
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
|
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
|
||||||
let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void
|
let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void
|
||||||
@ -248,7 +246,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
|
|
||||||
self.gridNode.visibleContentOffsetChanged = { [weak self] _ in
|
self.gridNode.visibleContentOffsetChanged = { [weak self] _ in
|
||||||
self?.updateNavigation(transition: .immediate)
|
self?.updateNavigation(transition: .immediate)
|
||||||
self?.updateScrollingArea()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.hiddenMediaDisposable = (self.hiddenMediaId.get()
|
self.hiddenMediaDisposable = (self.hiddenMediaId.get()
|
||||||
@ -325,7 +322,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
|
|
||||||
if self.controller?.collection != nil {
|
if self.controller?.collection != nil {
|
||||||
self.gridNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
self.gridNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
||||||
return point.x > 44.0 + overflowInset
|
return point.x > 44.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,7 +377,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.controller?.collection != nil {
|
if self.controller?.collection != nil {
|
||||||
self.selectionGesture?.sideInset = 44.0 + overflowInset
|
self.selectionGesture?.sideInset = 44.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,14 +403,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
guard let (layout, _) = self.validLayout else {
|
guard let (layout, _) = self.validLayout else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var tag: Int32?
|
var tag: Int32?
|
||||||
self.gridNode.forEachItemNode { itemNode in
|
self.gridNode.forEachItemNode { itemNode in
|
||||||
if let itemNode = itemNode as? MediaPickerGridItemNode {
|
if let itemNode = itemNode as? MediaPickerGridItemNode {
|
||||||
tag = itemNode.tag
|
tag = itemNode.tag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let dateString = tag.flatMap { self.scrollerTextForTag(tag: $0) }
|
let dateString = tag.flatMap { self.scrollerTextForTag(tag: $0) }
|
||||||
if self.currentScrollingTag != tag {
|
if self.currentScrollingTag != tag {
|
||||||
self.currentScrollingTag = tag
|
self.currentScrollingTag = tag
|
||||||
@ -421,7 +418,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self.scrollingArea.feedbackTap()
|
self.scrollingArea.feedbackTap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.scrollingArea.update(
|
self.scrollingArea.update(
|
||||||
containerSize: layout.size,
|
containerSize: layout.size,
|
||||||
containerInsets: self.gridNode.gridLayout.insets,
|
containerInsets: self.gridNode.gridLayout.insets,
|
||||||
@ -848,7 +845,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
insets.top += navigationBarHeight
|
insets.top += navigationBarHeight
|
||||||
|
|
||||||
let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height))
|
let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height))
|
||||||
let innerBounds = CGRect(origin: CGPoint(x: -overflowInset, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))
|
let innerBounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height))
|
||||||
|
|
||||||
let itemsPerRow: Int
|
let itemsPerRow: Int
|
||||||
if case .compact = layout.metrics.widthClass {
|
if case .compact = layout.metrics.widthClass {
|
||||||
@ -955,7 +952,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
transition.updateFrame(node: self.backgroundNode, frame: innerBounds)
|
transition.updateFrame(node: self.backgroundNode, frame: innerBounds)
|
||||||
self.backgroundNode.update(size: bounds.size, transition: transition)
|
self.backgroundNode.update(size: bounds.size, transition: transition)
|
||||||
|
|
||||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: overflowInset, y: 0.0), size: CGSize(width: bounds.width - overflowInset * 2.0, height: bounds.height)))
|
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: bounds.height)))
|
||||||
|
|
||||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemWidth, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in
|
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemWidth, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -1060,6 +1057,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
private let groupedPromise = ValuePromise<Bool>(true)
|
private let groupedPromise = ValuePromise<Bool>(true)
|
||||||
|
|
||||||
|
private var isDismissing = false
|
||||||
|
|
||||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, bannedSendMedia: (Int32, Bool)?, collection: PHAssetCollection? = nil, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil) {
|
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, bannedSendMedia: (Int32, Bool)?, collection: PHAssetCollection? = nil, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
@ -1160,7 +1159,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in
|
}, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in
|
||||||
if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState {
|
if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState, !strongSelf.isDismissing {
|
||||||
|
strongSelf.isDismissing = true
|
||||||
if let currentItem = currentItem {
|
if let currentItem = currentItem {
|
||||||
selectionState.setItem(currentItem, selected: true)
|
selectionState.setItem(currentItem, selected: true)
|
||||||
}
|
}
|
||||||
@ -1285,11 +1285,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
|
|
||||||
public func requestDismiss(completion: @escaping () -> Void) {
|
public func requestDismiss(completion: @escaping () -> Void) {
|
||||||
if let selectionState = self.interaction?.selectionState, selectionState.count() > 0 {
|
if let selectionState = self.interaction?.selectionState, selectionState.count() > 0 {
|
||||||
|
self.isDismissing = true
|
||||||
let controller = textAlertController(context: self.context, title: nil, text: self.presentationData.strings.Attachment_CancelSelectionAlertText, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertNo, action: {
|
let controller = textAlertController(context: self.context, title: nil, text: self.presentationData.strings.Attachment_CancelSelectionAlertText, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertNo, action: {
|
||||||
|
|
||||||
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertYes, action: {
|
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertYes, action: {
|
||||||
completion()
|
completion()
|
||||||
})])
|
})])
|
||||||
|
controller.dismissed = { [weak self] in
|
||||||
|
self?.isDismissing = false
|
||||||
|
}
|
||||||
self.present(controller, in: .window(.root))
|
self.present(controller, in: .window(.root))
|
||||||
} else {
|
} else {
|
||||||
completion()
|
completion()
|
||||||
|
@ -93,7 +93,7 @@ swift_library(
|
|||||||
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
||||||
"//submodules/WebPBinding:WebPBinding",
|
"//submodules/WebPBinding:WebPBinding",
|
||||||
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
|
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
|
||||||
"//submodules/Translate:Translate",
|
"//submodules/TranslateUI:TranslateUI",
|
||||||
"//submodules/QrCodeUI:QrCodeUI",
|
"//submodules/QrCodeUI:QrCodeUI",
|
||||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||||
|
@ -15,7 +15,7 @@ import SearchBarNode
|
|||||||
import SearchUI
|
import SearchUI
|
||||||
import UndoUI
|
import UndoUI
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import Translate
|
import TranslateUI
|
||||||
|
|
||||||
private enum LanguageListSection: ItemListSectionId {
|
private enum LanguageListSection: ItemListSectionId {
|
||||||
case translate
|
case translate
|
||||||
@ -470,7 +470,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
|||||||
entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: value))
|
entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: value))
|
||||||
entries.append(.translateInfo(text: ignoredLanguages.count > 1 ? presentationData.strings.Localization_DoNotTranslateManyInfo : presentationData.strings.Localization_DoNotTranslateInfo))
|
entries.append(.translateInfo(text: ignoredLanguages.count > 1 ? presentationData.strings.Localization_DoNotTranslateManyInfo : presentationData.strings.Localization_DoNotTranslateInfo))
|
||||||
} else {
|
} else {
|
||||||
entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfo))
|
entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfoExtended))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,7 +520,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
|||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.push(translationSettingsController(context: strongSelf.context))
|
strongSelf.push(translationSettingsController(context: strongSelf.context))
|
||||||
}
|
}
|
||||||
}, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count, crossfade: (previousState == nil) != (localizationListState == nil))
|
}, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) != entries.count, crossfade: (previousState == nil) != (localizationListState == nil))
|
||||||
strongSelf.enqueueTransition(transition)
|
strongSelf.enqueueTransition(transition)
|
||||||
})
|
})
|
||||||
self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start()
|
self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start()
|
||||||
|
@ -10,7 +10,7 @@ import ItemListUI
|
|||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import Translate
|
import TranslateUI
|
||||||
|
|
||||||
private final class TranslationSettingsControllerArguments {
|
private final class TranslationSettingsControllerArguments {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
@ -112,10 +112,10 @@ public func translationSettingsController(context: AccountContext) -> ViewContro
|
|||||||
}).start()
|
}).start()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
let enLocale = Locale(identifier: "en")
|
let enLocale = Locale(identifier: "en")
|
||||||
var languages: [(String, String, String)] = []
|
var languages: [(String, String, String)] = []
|
||||||
for code in supportedTranslationLanguages {
|
var addedLanguages = Set<String>()
|
||||||
|
for code in popularTranslationLanguages {
|
||||||
if let title = enLocale.localizedString(forLanguageCode: code) {
|
if let title = enLocale.localizedString(forLanguageCode: code) {
|
||||||
let languageLocale = Locale(identifier: code)
|
let languageLocale = Locale(identifier: code)
|
||||||
let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title
|
let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title
|
||||||
@ -125,6 +125,20 @@ public func translationSettingsController(context: AccountContext) -> ViewContro
|
|||||||
} else {
|
} else {
|
||||||
languages.append(value)
|
languages.append(value)
|
||||||
}
|
}
|
||||||
|
addedLanguages.insert(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for code in supportedTranslationLanguages {
|
||||||
|
if !addedLanguages.contains(code), let title = enLocale.localizedString(forLanguageCode: code) {
|
||||||
|
let languageLocale = Locale(identifier: code)
|
||||||
|
let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title
|
||||||
|
let value = (code, title.capitalized, subtitle.capitalized)
|
||||||
|
if code == interfaceLanguageCode {
|
||||||
|
languages.insert(value, at: 0)
|
||||||
|
} else {
|
||||||
|
languages.append(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,31 @@ import AVFoundation
|
|||||||
private final class LinkHelperClass: NSObject {
|
private final class LinkHelperClass: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func speakText(_ text: String) {
|
public class SpeechSynthesizerHolder: NSObject, AVSpeechSynthesizerDelegate {
|
||||||
|
private var speechSynthesizer: AVSpeechSynthesizer
|
||||||
|
|
||||||
|
public var completion: () -> Void = {}
|
||||||
|
|
||||||
|
init(speechSynthesizer: AVSpeechSynthesizer) {
|
||||||
|
self.speechSynthesizer = speechSynthesizer
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.speechSynthesizer.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stop() {
|
||||||
|
self.speechSynthesizer.stopSpeaking(at: .immediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
|
||||||
|
self.completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func speakText(_ text: String) -> SpeechSynthesizerHolder? {
|
||||||
guard !text.isEmpty else {
|
guard !text.isEmpty else {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
let speechSynthesizer = AVSpeechSynthesizer()
|
let speechSynthesizer = AVSpeechSynthesizer()
|
||||||
let utterance = AVSpeechUtterance(string: text)
|
let utterance = AVSpeechUtterance(string: text)
|
||||||
@ -15,4 +37,6 @@ public func speakText(_ text: String) {
|
|||||||
utterance.voice = AVSpeechSynthesisVoice(language: language)
|
utterance.voice = AVSpeechSynthesisVoice(language: language)
|
||||||
}
|
}
|
||||||
speechSynthesizer.speak(utterance)
|
speechSynthesizer.speak(utterance)
|
||||||
|
|
||||||
|
return SpeechSynthesizerHolder(speechSynthesizer: speechSynthesizer)
|
||||||
}
|
}
|
||||||
|
@ -641,9 +641,9 @@ public final class MediaStreamComponent: CombinedComponent {
|
|||||||
navigationRightItems.append(AnyComponentWithIdentity(id: "more", component: AnyComponent(Button(
|
navigationRightItems.append(AnyComponentWithIdentity(id: "more", component: AnyComponent(Button(
|
||||||
content: AnyComponent(ZStack([
|
content: AnyComponent(ZStack([
|
||||||
AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle(
|
AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle(
|
||||||
color: .white,
|
strokeColor: .white,
|
||||||
size: CGSize(width: 22.0, height: 22.0),
|
strokeWidth: 1.5,
|
||||||
width: 1.5
|
size: CGSize(width: 22.0, height: 22.0)
|
||||||
))),
|
))),
|
||||||
AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent(
|
AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent(
|
||||||
animation: LottieAnimationComponent.Animation(
|
animation: LottieAnimationComponent.Animation(
|
||||||
|
@ -537,7 +537,9 @@ private final class CallSessionManagerContext {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil))
|
strongSelf.contexts.removeValue(forKey: internalId)
|
||||||
|
strongSelf.contextIdByStableId.removeValue(forKey: stableId)
|
||||||
|
strongSelf.ringingStatesUpdated()
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
self.contextIdByStableId[stableId] = internalId
|
self.contextIdByStableId[stableId] = internalId
|
||||||
|
@ -30,15 +30,16 @@ private func generateInstantVideoBackground(fillColor: UIColor, strokeColor: UIC
|
|||||||
private func generateActionPhotoBackground(fillColor: UIColor, strokeColor: UIColor) -> UIImage? {
|
private func generateActionPhotoBackground(fillColor: UIColor, strokeColor: UIColor) -> UIImage? {
|
||||||
return generateImage(CGSize(width: 214.0, height: 214.0), rotatedContext: { size, context in
|
return generateImage(CGSize(width: 214.0, height: 214.0), rotatedContext: { size, context in
|
||||||
let lineWidth: CGFloat = 0.5
|
let lineWidth: CGFloat = 0.5
|
||||||
|
let cornerRadius: CGFloat = 16.0
|
||||||
|
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
context.setFillColor(strokeColor.cgColor)
|
context.setFillColor(strokeColor.cgColor)
|
||||||
let strokePath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 15.0)
|
let strokePath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius)
|
||||||
context.addPath(strokePath.cgPath)
|
context.addPath(strokePath.cgPath)
|
||||||
context.fillPath()
|
context.fillPath()
|
||||||
context.setFillColor(fillColor.cgColor)
|
context.setFillColor(fillColor.cgColor)
|
||||||
let fillPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: lineWidth, y: lineWidth), size: CGSize(width: size.width - lineWidth * 2.0, height: size.height - lineWidth * 2.0)), cornerRadius: 15.0)
|
let fillPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: lineWidth, y: lineWidth), size: CGSize(width: size.width - lineWidth * 2.0, height: size.height - lineWidth * 2.0)), cornerRadius: cornerRadius)
|
||||||
context.addPath(fillPath.cgPath)
|
context.addPath(fillPath.cgPath)
|
||||||
context.fillPath()
|
context.fillPath()
|
||||||
})
|
})
|
||||||
|
@ -252,7 +252,6 @@ swift_library(
|
|||||||
"//submodules/QrCodeUI:QrCodeUI",
|
"//submodules/QrCodeUI:QrCodeUI",
|
||||||
"//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent",
|
"//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent",
|
||||||
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
|
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
|
||||||
"//submodules/Translate:Translate",
|
|
||||||
"//submodules/TabBarUI:TabBarUI",
|
"//submodules/TabBarUI:TabBarUI",
|
||||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||||
"//submodules/ManagedFile:ManagedFile",
|
"//submodules/ManagedFile:ManagedFile",
|
||||||
@ -266,6 +265,7 @@ swift_library(
|
|||||||
"//submodules/MediaPickerUI:MediaPickerUI",
|
"//submodules/MediaPickerUI:MediaPickerUI",
|
||||||
"//submodules/ChatMessageBackground:ChatMessageBackground",
|
"//submodules/ChatMessageBackground:ChatMessageBackground",
|
||||||
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
|
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
|
||||||
|
"//submodules/TranslateUI:TranslateUI",
|
||||||
] + select({
|
] + select({
|
||||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||||
|
@ -60,7 +60,7 @@ import InviteLinksUI
|
|||||||
import Markdown
|
import Markdown
|
||||||
import TelegramPermissionsUI
|
import TelegramPermissionsUI
|
||||||
import Speak
|
import Speak
|
||||||
import Translate
|
import TranslateUI
|
||||||
import UniversalMediaPlayer
|
import UniversalMediaPlayer
|
||||||
import WallpaperBackgroundNode
|
import WallpaperBackgroundNode
|
||||||
import ChatListUI
|
import ChatListUI
|
||||||
@ -2915,7 +2915,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
window.rootViewController?.present(controller, animated: true)
|
window.rootViewController?.present(controller, animated: true)
|
||||||
}
|
}
|
||||||
case .speak:
|
case .speak:
|
||||||
speakText(text.string)
|
let _ = speakText(text.string)
|
||||||
case .translate:
|
case .translate:
|
||||||
let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|
let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|
||||||
|> take(1)
|
|> take(1)
|
||||||
@ -2929,7 +2929,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
|
|
||||||
let (_, language) = canTranslateText(context: context, text: text.string, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
|
let (_, language) = canTranslateText(context: context, text: text.string, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
|
||||||
|
|
||||||
translateText(context: context, text: text.string, fromLang: language)
|
let controller = TranslateScreen(context: context, text: text.string, fromLanguage: language)
|
||||||
|
controller.pushController = { [weak self] c in
|
||||||
|
self?.effectiveNavigationController?._keepModalDismissProgress = true
|
||||||
|
self?.push(c)
|
||||||
|
}
|
||||||
|
controller.presentController = { [weak self] c in
|
||||||
|
self?.present(c, in: .window(.root))
|
||||||
|
}
|
||||||
|
strongSelf.present(controller, in: .window(.root))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, displayImportedMessageTooltip: { [weak self] _ in
|
}, displayImportedMessageTooltip: { [weak self] _ in
|
||||||
@ -10630,7 +10638,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
|
|
||||||
if inputIsActive {
|
if inputIsActive {
|
||||||
Queue.mainQueue().after(0.1, {
|
Queue.mainQueue().after(0.15, {
|
||||||
present()
|
present()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -25,7 +25,7 @@ import AdUI
|
|||||||
import TelegramNotices
|
import TelegramNotices
|
||||||
import ReactionListContextMenuContent
|
import ReactionListContextMenuContent
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import Translate
|
import TranslateUI
|
||||||
import DebugSettingsUI
|
import DebugSettingsUI
|
||||||
import ChatPresentationInterfaceState
|
import ChatPresentationInterfaceState
|
||||||
import Pasteboard
|
import Pasteboard
|
||||||
|
@ -2316,7 +2316,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string
|
text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string
|
||||||
return (current, inputMode)
|
return (current, inputMode)
|
||||||
}
|
}
|
||||||
speakText(text)
|
let _ = speakText(text)
|
||||||
|
|
||||||
if #available(iOS 13.0, *) {
|
if #available(iOS 13.0, *) {
|
||||||
UIMenuController.shared.hideMenu()
|
UIMenuController.shared.hideMenu()
|
||||||
|
@ -492,7 +492,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let controller = ImportStickerPackController(context: context, stickerPack: stickerPack, parentNavigationController: navigationController)
|
let controller = ImportStickerPackController(context: context, stickerPack: stickerPack, parentNavigationController: navigationController)
|
||||||
present(controller, nil)
|
Queue.mainQueue().after(0.3) {
|
||||||
|
present(controller, nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ import PasswordSetupUI
|
|||||||
import CalendarMessageScreen
|
import CalendarMessageScreen
|
||||||
import TooltipUI
|
import TooltipUI
|
||||||
import QrCodeUI
|
import QrCodeUI
|
||||||
import Translate
|
import TranslateUI
|
||||||
import ChatPresentationInterfaceState
|
import ChatPresentationInterfaceState
|
||||||
import CreateExternalMediaStreamScreen
|
import CreateExternalMediaStreamScreen
|
||||||
|
|
||||||
@ -5017,8 +5017,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
|
|
||||||
let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
|
let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
|
||||||
if canTranslate {
|
if canTranslate {
|
||||||
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuTranslate, accessibilityLabel: presentationData.strings.Conversation_ContextMenuTranslate), action: {
|
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuTranslate, accessibilityLabel: presentationData.strings.Conversation_ContextMenuTranslate), action: { [weak self] in
|
||||||
translateText(context: context, text: text, fromLang: language)
|
|
||||||
|
let controller = TranslateScreen(context: context, text: text, fromLanguage: language)
|
||||||
|
self?.controller?.present(controller, in: .window(.root))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
|
||||||
|
|
||||||
swift_library(
|
|
||||||
name = "Translate",
|
|
||||||
module_name = "Translate",
|
|
||||||
srcs = glob([
|
|
||||||
"Sources/**/*.swift",
|
|
||||||
]),
|
|
||||||
copts = [
|
|
||||||
"-warnings-as-errors",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
"//submodules/Display:Display",
|
|
||||||
"//submodules/AccountContext:AccountContext",
|
|
||||||
],
|
|
||||||
visibility = [
|
|
||||||
"//visibility:public",
|
|
||||||
],
|
|
||||||
)
|
|
@ -1,82 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import Display
|
|
||||||
import AccountContext
|
|
||||||
import NaturalLanguage
|
|
||||||
import TelegramCore
|
|
||||||
|
|
||||||
// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker
|
|
||||||
private final class LinkHelperClass: NSObject {
|
|
||||||
}
|
|
||||||
|
|
||||||
public var supportedTranslationLanguages = [
|
|
||||||
"en",
|
|
||||||
"ar",
|
|
||||||
"zh-Hans",
|
|
||||||
"zh-Hant",
|
|
||||||
"fr",
|
|
||||||
"de",
|
|
||||||
"it",
|
|
||||||
"ja",
|
|
||||||
"ko",
|
|
||||||
"pt",
|
|
||||||
"ru",
|
|
||||||
"es"
|
|
||||||
]
|
|
||||||
|
|
||||||
@available(iOS 12.0, *)
|
|
||||||
private let languageRecognizer = NLLanguageRecognizer()
|
|
||||||
|
|
||||||
public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, ignoredLanguages: [String]?) -> (canTranslate: Bool, language: String?) {
|
|
||||||
guard showTranslate, text.count > 0 else {
|
|
||||||
return (false, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if #available(iOS 15.0, *) {
|
|
||||||
var dontTranslateLanguages: [String] = []
|
|
||||||
if let ignoredLanguages = ignoredLanguages {
|
|
||||||
dontTranslateLanguages = ignoredLanguages
|
|
||||||
} else {
|
|
||||||
dontTranslateLanguages = [context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode]
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = String(text.prefix(64))
|
|
||||||
languageRecognizer.processString(text)
|
|
||||||
let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 3)
|
|
||||||
languageRecognizer.reset()
|
|
||||||
|
|
||||||
let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains($0.key.rawValue) }.sorted(by: { $0.value > $1.value })
|
|
||||||
if let language = filteredLanguages.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) {
|
|
||||||
return (!dontTranslateLanguages.contains(language.key.rawValue), language.key.rawValue)
|
|
||||||
} else {
|
|
||||||
return (false, nil)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return (false, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func translateText(context: AccountContext, text: String, fromLang: String? = nil) {
|
|
||||||
guard !text.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if #available(iOS 15.0, *) {
|
|
||||||
let text = text.unicodeScalars.filter { !$0.properties.isEmojiPresentation }.reduce("") { $0 + String($1) }
|
|
||||||
|
|
||||||
let textView = UITextView()
|
|
||||||
textView.text = text
|
|
||||||
textView.isEditable = false
|
|
||||||
if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController {
|
|
||||||
topController.view.addSubview(textView)
|
|
||||||
textView.selectAll(nil)
|
|
||||||
textView.perform(NSSelectorFromString(["_", "trans", "late:"].joined(separator: "")), with: nil)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
textView.removeFromSuperview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let toLang = context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode
|
|
||||||
let _ = context.engine.messages.translate(text: text, fromLang: fromLang, toLang: toLang).start()
|
|
||||||
}
|
|
||||||
}
|
|
38
submodules/TranslateUI/BUILD
Normal file
38
submodules/TranslateUI/BUILD
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "TranslateUI",
|
||||||
|
module_name = "TranslateUI",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/TelegramCore:TelegramCore",
|
||||||
|
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||||
|
"//submodules/AccountContext:AccountContext",
|
||||||
|
"//submodules/AlertUI:AlertUI",
|
||||||
|
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||||
|
"//submodules/Speak:Speak",
|
||||||
|
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||||
|
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||||
|
"//submodules/ItemListUI:ItemListUI",
|
||||||
|
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||||
|
"//submodules/ComponentFlow:ComponentFlow",
|
||||||
|
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
||||||
|
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||||
|
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||||
|
"//submodules/UndoUI:UndoUI",
|
||||||
|
"//submodules/ActivityIndicator:ActivityIndicator",
|
||||||
|
"//submodules/ChatListSearchItemNode:ChatListSearchItemNode",
|
||||||
|
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
194
submodules/TranslateUI/Sources/LanguageSelectionController.swift
Normal file
194
submodules/TranslateUI/Sources/LanguageSelectionController.swift
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import TelegramUIPreferences
|
||||||
|
import ItemListUI
|
||||||
|
import PresentationDataUtils
|
||||||
|
import TelegramStringFormatting
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
|
private final class LanguageSelectionControllerArguments {
|
||||||
|
let context: AccountContext
|
||||||
|
let updateLanguageSelected: (String) -> Void
|
||||||
|
|
||||||
|
init(context: AccountContext, updateLanguageSelected: @escaping (String) -> Void) {
|
||||||
|
self.context = context
|
||||||
|
self.updateLanguageSelected = updateLanguageSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum LanguageSelectionControllerSection: Int32 {
|
||||||
|
case languages
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum LanguageSelectionControllerEntry: ItemListNodeEntry {
|
||||||
|
case language(Int32, PresentationTheme, String, String, Bool, String)
|
||||||
|
|
||||||
|
var section: ItemListSectionId {
|
||||||
|
switch self {
|
||||||
|
case .language:
|
||||||
|
return LanguageSelectionControllerSection.languages.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stableId: Int32 {
|
||||||
|
switch self {
|
||||||
|
case let .language(index, _, _, _, _, _):
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: LanguageSelectionControllerEntry, rhs: LanguageSelectionControllerEntry) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case let .language(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsValue, lhsCode):
|
||||||
|
if case let .language(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsValue, rhsCode) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsValue == rhsValue, lhsCode == rhsCode {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func <(lhs: LanguageSelectionControllerEntry, rhs: LanguageSelectionControllerEntry) -> Bool {
|
||||||
|
return lhs.stableId < rhs.stableId
|
||||||
|
}
|
||||||
|
|
||||||
|
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||||
|
let arguments = arguments as! LanguageSelectionControllerArguments
|
||||||
|
switch self {
|
||||||
|
case let .language(_, _, title, subtitle, value, code):
|
||||||
|
return LocalizationListItem(presentationData: presentationData, id: code, title: title, subtitle: subtitle, checked: value, activity: false, loading: false, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), sectionId: self.section, alwaysPlain: false, action: {
|
||||||
|
arguments.updateLanguageSelected(code)
|
||||||
|
}, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func languageSelectionControllerEntries(theme: PresentationTheme, strings: PresentationStrings, selectedLanguage: String, languages: [(String, String, String)]) -> [LanguageSelectionControllerEntry] {
|
||||||
|
var entries: [LanguageSelectionControllerEntry] = []
|
||||||
|
|
||||||
|
var index: Int32 = 0
|
||||||
|
for (code, title, subtitle) in languages {
|
||||||
|
entries.append(.language(index, theme, title, subtitle, code == selectedLanguage, code))
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct LanguageSelectionControllerState: Equatable {
|
||||||
|
enum Section {
|
||||||
|
case original
|
||||||
|
case translation
|
||||||
|
}
|
||||||
|
|
||||||
|
var section: Section
|
||||||
|
var fromLanguage: String
|
||||||
|
var toLanguage: String
|
||||||
|
}
|
||||||
|
|
||||||
|
public func languageSelectionController(context: AccountContext, fromLanguage: String, toLanguage: String, completion: @escaping (String, String) -> Void) -> ViewController {
|
||||||
|
let statePromise = ValuePromise(LanguageSelectionControllerState(section: .translation, fromLanguage: fromLanguage, toLanguage: toLanguage), ignoreRepeated: true)
|
||||||
|
let stateValue = Atomic(value: LanguageSelectionControllerState(section: .translation, fromLanguage: fromLanguage, toLanguage: toLanguage))
|
||||||
|
let updateState: ((LanguageSelectionControllerState) -> LanguageSelectionControllerState) -> Void = { f in
|
||||||
|
statePromise.set(stateValue.modify { f($0) })
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionsDisposable = DisposableSet()
|
||||||
|
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let interfaceLanguageCode = presentationData.strings.baseLanguageCode
|
||||||
|
|
||||||
|
var dismissImpl: (() -> Void)?
|
||||||
|
|
||||||
|
let arguments = LanguageSelectionControllerArguments(context: context, updateLanguageSelected: { code in
|
||||||
|
updateState { current in
|
||||||
|
var updated = current
|
||||||
|
switch updated.section {
|
||||||
|
case .original:
|
||||||
|
updated.fromLanguage = code
|
||||||
|
case .translation:
|
||||||
|
updated.toLanguage = code
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let enLocale = Locale(identifier: "en")
|
||||||
|
var languages: [(String, String, String)] = []
|
||||||
|
var addedLanguages = Set<String>()
|
||||||
|
for code in popularTranslationLanguages {
|
||||||
|
if let title = enLocale.localizedString(forLanguageCode: code) {
|
||||||
|
let languageLocale = Locale(identifier: code)
|
||||||
|
let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title
|
||||||
|
let value = (code, title.capitalized, subtitle.capitalized)
|
||||||
|
if code == interfaceLanguageCode {
|
||||||
|
languages.insert(value, at: 0)
|
||||||
|
} else {
|
||||||
|
languages.append(value)
|
||||||
|
}
|
||||||
|
addedLanguages.insert(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for code in supportedTranslationLanguages {
|
||||||
|
if !addedLanguages.contains(code), let title = enLocale.localizedString(forLanguageCode: code) {
|
||||||
|
let languageLocale = Locale(identifier: code)
|
||||||
|
let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title
|
||||||
|
let value = (code, title.capitalized, subtitle.capitalized)
|
||||||
|
if code == interfaceLanguageCode {
|
||||||
|
languages.insert(value, at: 0)
|
||||||
|
} else {
|
||||||
|
languages.append(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let signal = combineLatest(queue: Queue.mainQueue(), context.sharedContext.presentationData, statePromise.get())
|
||||||
|
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||||
|
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.Translate_Languages_Original, presentationData.strings.Translate_Languages_Translation], 1), leftNavigationButton: ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}), rightNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
|
||||||
|
completion(state.fromLanguage, state.toLanguage)
|
||||||
|
dismissImpl?()
|
||||||
|
}), backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
||||||
|
|
||||||
|
let selectedLanguage: String
|
||||||
|
switch state.section {
|
||||||
|
case.original:
|
||||||
|
selectedLanguage = state.fromLanguage
|
||||||
|
case .translation:
|
||||||
|
selectedLanguage = state.toLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: languageSelectionControllerEntries(theme: presentationData.theme, strings: presentationData.strings, selectedLanguage: selectedLanguage, languages: languages), style: .blocks, animateChanges: false)
|
||||||
|
|
||||||
|
return (controllerState, (listState, arguments))
|
||||||
|
}
|
||||||
|
|> afterDisposed {
|
||||||
|
actionsDisposable.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller = ItemListController(context: context, state: signal)
|
||||||
|
controller.titleControlValueChanged = { value in
|
||||||
|
updateState { current in
|
||||||
|
var updated = current
|
||||||
|
if value == 0 {
|
||||||
|
updated.section = .original
|
||||||
|
} else {
|
||||||
|
updated.section = .translation
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.alwaysSynchronous = true
|
||||||
|
controller.navigationPresentation = .modal
|
||||||
|
|
||||||
|
dismissImpl = { [weak controller] in
|
||||||
|
controller?.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return controller
|
||||||
|
}
|
@ -10,14 +10,21 @@ import ActivityIndicator
|
|||||||
import ChatListSearchItemNode
|
import ChatListSearchItemNode
|
||||||
import ShimmerEffect
|
import ShimmerEffect
|
||||||
|
|
||||||
struct LocalizationListItemEditing: Equatable {
|
public struct LocalizationListItemEditing: Equatable {
|
||||||
let editable: Bool
|
let editable: Bool
|
||||||
let editing: Bool
|
let editing: Bool
|
||||||
let revealed: Bool
|
let revealed: Bool
|
||||||
let reorderable: Bool
|
let reorderable: Bool
|
||||||
|
|
||||||
|
public init(editable: Bool, editing: Bool, revealed: Bool, reorderable: Bool) {
|
||||||
|
self.editable = editable
|
||||||
|
self.editing = editing
|
||||||
|
self.revealed = revealed
|
||||||
|
self.reorderable = reorderable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalizationListItem: ListViewItem, ItemListItem {
|
public class LocalizationListItem: ListViewItem, ItemListItem {
|
||||||
let presentationData: ItemListPresentationData
|
let presentationData: ItemListPresentationData
|
||||||
let id: String
|
let id: String
|
||||||
let title: String
|
let title: String
|
||||||
@ -26,13 +33,14 @@ class LocalizationListItem: ListViewItem, ItemListItem {
|
|||||||
let activity: Bool
|
let activity: Bool
|
||||||
let loading: Bool
|
let loading: Bool
|
||||||
let editing: LocalizationListItemEditing
|
let editing: LocalizationListItemEditing
|
||||||
let sectionId: ItemListSectionId
|
let enabled: Bool
|
||||||
|
public let sectionId: ItemListSectionId
|
||||||
let alwaysPlain: Bool
|
let alwaysPlain: Bool
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
let setItemWithRevealedOptions: (String?, String?) -> Void
|
let setItemWithRevealedOptions: (String?, String?) -> Void
|
||||||
let removeItem: (String) -> Void
|
let removeItem: (String) -> Void
|
||||||
|
|
||||||
init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) {
|
public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) {
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
@ -41,6 +49,7 @@ class LocalizationListItem: ListViewItem, ItemListItem {
|
|||||||
self.activity = activity
|
self.activity = activity
|
||||||
self.loading = loading
|
self.loading = loading
|
||||||
self.editing = editing
|
self.editing = editing
|
||||||
|
self.enabled = enabled
|
||||||
self.sectionId = sectionId
|
self.sectionId = sectionId
|
||||||
self.alwaysPlain = alwaysPlain
|
self.alwaysPlain = alwaysPlain
|
||||||
self.action = action
|
self.action = action
|
||||||
@ -48,7 +57,7 @@ class LocalizationListItem: ListViewItem, ItemListItem {
|
|||||||
self.removeItem = removeItem
|
self.removeItem = removeItem
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||||
async {
|
async {
|
||||||
let node = LocalizationListItemNode()
|
let node = LocalizationListItemNode()
|
||||||
var neighbors = itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)
|
var neighbors = itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)
|
||||||
@ -68,7 +77,7 @@ class LocalizationListItem: ListViewItem, ItemListItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
if let nodeValue = node() as? LocalizationListItemNode {
|
if let nodeValue = node() as? LocalizationListItemNode {
|
||||||
let makeLayout = nodeValue.asyncLayout()
|
let makeLayout = nodeValue.asyncLayout()
|
||||||
@ -89,9 +98,9 @@ class LocalizationListItem: ListViewItem, ItemListItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectable: Bool = true
|
public var selectable: Bool = true
|
||||||
|
|
||||||
func selected(listView: ListView){
|
public func selected(listView: ListView){
|
||||||
listView.clearHighlightAnimated(true)
|
listView.clearHighlightAnimated(true)
|
||||||
self.action()
|
self.action()
|
||||||
}
|
}
|
113
submodules/TranslateUI/Sources/PlayPauseIconCompoennt.swift
Normal file
113
submodules/TranslateUI/Sources/PlayPauseIconCompoennt.swift
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import ComponentFlow
|
||||||
|
import ManagedAnimationNode
|
||||||
|
|
||||||
|
enum PlayPauseIconNodeState: Equatable {
|
||||||
|
case play
|
||||||
|
case pause
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PlayPauseIconNode: ManagedAnimationNode {
|
||||||
|
private let duration: Double = 0.35
|
||||||
|
private var iconState: PlayPauseIconNodeState = .play
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(size: CGSize(width: 40.0, height: 40.0))
|
||||||
|
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
|
||||||
|
guard self.iconState != state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousState = self.iconState
|
||||||
|
self.iconState = state
|
||||||
|
|
||||||
|
switch previousState {
|
||||||
|
case .pause:
|
||||||
|
switch state {
|
||||||
|
case .play:
|
||||||
|
if animated {
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
|
||||||
|
} else {
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||||
|
}
|
||||||
|
case .pause:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case .play:
|
||||||
|
switch state {
|
||||||
|
case .pause:
|
||||||
|
if animated {
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
|
||||||
|
} else {
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
|
||||||
|
}
|
||||||
|
case .play:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PlayPauseIconComponent: Component {
|
||||||
|
let state: PlayPauseIconNodeState
|
||||||
|
let size: CGSize
|
||||||
|
|
||||||
|
init(state: PlayPauseIconNodeState, size: CGSize) {
|
||||||
|
self.state = state
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: PlayPauseIconComponent, rhs: PlayPauseIconComponent) -> Bool {
|
||||||
|
if lhs.state != rhs.state {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.size != rhs.size {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private var component: PlayPauseIconComponent?
|
||||||
|
private var animationNode: PlayPauseIconNode
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.animationNode = PlayPauseIconNode()
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.animationNode.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: PlayPauseIconComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
if self.component?.state != component.state {
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
self.animationNode.enqueueState(component.state, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let animationSize = component.size
|
||||||
|
let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height))
|
||||||
|
self.animationNode.view.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
271
submodules/TranslateUI/Sources/Translate.swift
Normal file
271
submodules/TranslateUI/Sources/Translate.swift
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AccountContext
|
||||||
|
import NaturalLanguage
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
|
// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker
|
||||||
|
private final class LinkHelperClass: NSObject {
|
||||||
|
}
|
||||||
|
|
||||||
|
public var supportedTranslationLanguages = [
|
||||||
|
"af",
|
||||||
|
"sq",
|
||||||
|
"am",
|
||||||
|
"ar",
|
||||||
|
"hy",
|
||||||
|
"az",
|
||||||
|
"eu",
|
||||||
|
"be",
|
||||||
|
"bn",
|
||||||
|
"bs",
|
||||||
|
"bg",
|
||||||
|
"ca",
|
||||||
|
"ceb",
|
||||||
|
"zh-Hans",
|
||||||
|
// "zh-Hant",
|
||||||
|
// "zh-CN", "zh"
|
||||||
|
// "zh-TW"
|
||||||
|
"co",
|
||||||
|
"hr",
|
||||||
|
"cs",
|
||||||
|
"da",
|
||||||
|
"nl",
|
||||||
|
"en",
|
||||||
|
"eo",
|
||||||
|
"et",
|
||||||
|
"fi",
|
||||||
|
"fr",
|
||||||
|
"fy",
|
||||||
|
"gl",
|
||||||
|
"ka",
|
||||||
|
"de",
|
||||||
|
"el",
|
||||||
|
"gu",
|
||||||
|
"ht",
|
||||||
|
"ha",
|
||||||
|
"haw",
|
||||||
|
"he",
|
||||||
|
"hi",
|
||||||
|
"hmn",
|
||||||
|
"hu",
|
||||||
|
"is",
|
||||||
|
"ig",
|
||||||
|
"id",
|
||||||
|
"ga",
|
||||||
|
"it",
|
||||||
|
"ja",
|
||||||
|
"jv",
|
||||||
|
"kn",
|
||||||
|
"kk",
|
||||||
|
"km",
|
||||||
|
"rw",
|
||||||
|
"ko",
|
||||||
|
"ku",
|
||||||
|
"ky",
|
||||||
|
"lo",
|
||||||
|
"lv",
|
||||||
|
"lt",
|
||||||
|
"lb",
|
||||||
|
"mk",
|
||||||
|
"mg",
|
||||||
|
"ms",
|
||||||
|
"ml",
|
||||||
|
"mt",
|
||||||
|
"mi",
|
||||||
|
"mr",
|
||||||
|
"mn",
|
||||||
|
"my",
|
||||||
|
"ne",
|
||||||
|
"no",
|
||||||
|
"ny",
|
||||||
|
"or",
|
||||||
|
"ps",
|
||||||
|
"fa",
|
||||||
|
"pl",
|
||||||
|
"pt",
|
||||||
|
"pa",
|
||||||
|
"ro",
|
||||||
|
"ru",
|
||||||
|
"sm",
|
||||||
|
"gd",
|
||||||
|
"sr",
|
||||||
|
"st",
|
||||||
|
"sn",
|
||||||
|
"sd",
|
||||||
|
"si",
|
||||||
|
"sk",
|
||||||
|
"sl",
|
||||||
|
"so",
|
||||||
|
"es",
|
||||||
|
"su",
|
||||||
|
"sw",
|
||||||
|
"sv",
|
||||||
|
"tl",
|
||||||
|
"tg",
|
||||||
|
"ta",
|
||||||
|
"tt",
|
||||||
|
"te",
|
||||||
|
"th",
|
||||||
|
"tr",
|
||||||
|
"tk",
|
||||||
|
"uk",
|
||||||
|
"ur",
|
||||||
|
"ug",
|
||||||
|
"uz",
|
||||||
|
"vi",
|
||||||
|
"cy",
|
||||||
|
"xh",
|
||||||
|
"yi",
|
||||||
|
"yo",
|
||||||
|
"zu"
|
||||||
|
]
|
||||||
|
|
||||||
|
public var popularTranslationLanguages = [
|
||||||
|
"en",
|
||||||
|
"ar",
|
||||||
|
"zh-Hans",
|
||||||
|
// "zh-Hant",
|
||||||
|
"fr",
|
||||||
|
"de",
|
||||||
|
"it",
|
||||||
|
"ja",
|
||||||
|
"ko",
|
||||||
|
"pt",
|
||||||
|
"ru",
|
||||||
|
"es"
|
||||||
|
]
|
||||||
|
|
||||||
|
@available(iOS 12.0, *)
|
||||||
|
private let languageRecognizer = NLLanguageRecognizer()
|
||||||
|
|
||||||
|
public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, ignoredLanguages: [String]?) -> (canTranslate: Bool, language: String?) {
|
||||||
|
guard showTranslate, text.count > 0 else {
|
||||||
|
return (false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
var dontTranslateLanguages: [String] = []
|
||||||
|
if let ignoredLanguages = ignoredLanguages {
|
||||||
|
dontTranslateLanguages = ignoredLanguages
|
||||||
|
} else {
|
||||||
|
dontTranslateLanguages = [context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode]
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = String(text.prefix(64))
|
||||||
|
languageRecognizer.processString(text)
|
||||||
|
let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 3)
|
||||||
|
languageRecognizer.reset()
|
||||||
|
|
||||||
|
let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains($0.key.rawValue) }.sorted(by: { $0.value > $1.value })
|
||||||
|
if let language = filteredLanguages.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) {
|
||||||
|
return (!dontTranslateLanguages.contains(language.key.rawValue), language.key.rawValue)
|
||||||
|
} else {
|
||||||
|
return (false, nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (false, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func translateText(context: AccountContext, text: String, fromLang: String? = nil) {
|
||||||
|
guard !text.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
let text = text.unicodeScalars.filter { !$0.properties.isEmojiPresentation }.reduce("") { $0 + String($1) }
|
||||||
|
|
||||||
|
let textView = UITextView()
|
||||||
|
textView.text = text
|
||||||
|
textView.isEditable = false
|
||||||
|
if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController {
|
||||||
|
topController.view.addSubview(textView)
|
||||||
|
textView.selectAll(nil)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
textView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let toLang = context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode
|
||||||
|
let _ = context.engine.messages.translate(text: text, fromLang: fromLang, toLang: toLang).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct TextTranslationResult: Equatable {
|
||||||
|
let text: String
|
||||||
|
let detectedLanguage: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TextTranslationError {
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
private let userAgents: [String] = [
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36", // 13.5%
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", // 6.6%
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0", // 6.4%
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", // 6.2%
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36", // 5.2%
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36" // 4.8%
|
||||||
|
]
|
||||||
|
|
||||||
|
public func translateText(context: AccountContext, text: String, from: String?, to: String) -> Signal<TextTranslationResult, TextTranslationError> {
|
||||||
|
return Signal { subscriber in
|
||||||
|
var uri = "https://translate.goo";
|
||||||
|
uri += "gleapis.com/transl";
|
||||||
|
uri += "ate_a";
|
||||||
|
uri += "/singl";
|
||||||
|
uri += "e?client=gtx&sl=" + (from ?? "auto") + "&tl=" + to + "&dt=t" + "&ie=UTF-8&oe=UTF-8&otf=1&ssel=0&tsel=0&kc=7&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&q=";
|
||||||
|
uri += text.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)!
|
||||||
|
|
||||||
|
var request = URLRequest(url: URL(string: uri)!)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue(userAgents[Int.random(in: 0 ..< userAgents.count)], forHTTPHeaderField: "User-Agent")
|
||||||
|
let session = URLSession.shared
|
||||||
|
let task = session.dataTask(with: request, completionHandler: { data, response, error in
|
||||||
|
if let _ = error {
|
||||||
|
subscriber.putError(.generic)
|
||||||
|
} else if let data = data {
|
||||||
|
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? NSArray
|
||||||
|
if let json = json, json.count > 0 {
|
||||||
|
let array = json[0] as? NSArray ?? NSArray()
|
||||||
|
var result: String = ""
|
||||||
|
for i in 0 ..< array.count {
|
||||||
|
let blockText = array[i] as? NSArray
|
||||||
|
if let blockText = blockText, blockText.count > 0 {
|
||||||
|
let value = blockText[0] as? String
|
||||||
|
if let value = value, value != "null" {
|
||||||
|
result += value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let translationResult = TextTranslationResult(text: result, detectedLanguage: json[2] as? String)
|
||||||
|
|
||||||
|
var fromLang: String?
|
||||||
|
if let lang = translationResult.detectedLanguage {
|
||||||
|
fromLang = lang
|
||||||
|
} else if let lang = from {
|
||||||
|
fromLang = lang
|
||||||
|
}
|
||||||
|
if let fromLang = fromLang {
|
||||||
|
let _ = context.engine.messages.translate(text: text, fromLang: fromLang, toLang: to).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber.putNext(translationResult)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
} else {
|
||||||
|
subscriber.putError(.generic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
task.resume()
|
||||||
|
|
||||||
|
return ActionDisposable {
|
||||||
|
task.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
183
submodules/TranslateUI/Sources/TranslateButtonComponent.swift
Normal file
183
submodules/TranslateUI/Sources/TranslateButtonComponent.swift
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import TelegramPresentationData
|
||||||
|
import BundleIconComponent
|
||||||
|
|
||||||
|
private final class TranslateButtonContentComponent: CombinedComponent {
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let title: String
|
||||||
|
let icon: String
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
title: String,
|
||||||
|
icon: String
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.title = title
|
||||||
|
self.icon = icon
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: TranslateButtonContentComponent, rhs: TranslateButtonContentComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.icon != rhs.icon {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static var body: Body {
|
||||||
|
let title = Child(Text.self)
|
||||||
|
let icon = Child(BundleIconComponent.self)
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let component = context.component
|
||||||
|
|
||||||
|
let icon = icon.update(
|
||||||
|
component: BundleIconComponent(
|
||||||
|
name: component.icon,
|
||||||
|
tintColor: component.theme.list.itemPrimaryTextColor
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let title = title.update(
|
||||||
|
component: Text(
|
||||||
|
text: component.title,
|
||||||
|
font: Font.regular(17.0),
|
||||||
|
color: component.theme.list.itemPrimaryTextColor
|
||||||
|
),
|
||||||
|
availableSize: context.availableSize,
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 16.0
|
||||||
|
|
||||||
|
context.add(title
|
||||||
|
.position(CGPoint(x: sideInset + title.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(icon
|
||||||
|
.position(CGPoint(x: context.availableSize.width - sideInset - icon.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
return context.availableSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TranslateButtonComponent: Component {
|
||||||
|
private let content: TranslateButtonContentComponent
|
||||||
|
private let theme: PresentationTheme
|
||||||
|
private let isEnabled: Bool
|
||||||
|
private let action: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
title: String,
|
||||||
|
icon: String,
|
||||||
|
isEnabled: Bool,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.content = TranslateButtonContentComponent(theme: theme, title: title, icon: icon)
|
||||||
|
self.isEnabled = isEnabled
|
||||||
|
self.theme = theme
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: TranslateButtonComponent, rhs: TranslateButtonComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.content !== rhs.content {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.isEnabled != rhs.isEnabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: HighlightTrackingButton {
|
||||||
|
private let backgroundView: UIView
|
||||||
|
private let centralContentView: ComponentHostView<Empty>
|
||||||
|
|
||||||
|
private var component: TranslateButtonComponent?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.backgroundView = UIView()
|
||||||
|
self.backgroundView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.centralContentView = ComponentHostView()
|
||||||
|
self.centralContentView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.backgroundView.clipsToBounds = true
|
||||||
|
|
||||||
|
self.addSubview(self.backgroundView)
|
||||||
|
self.addSubview(self.centralContentView)
|
||||||
|
|
||||||
|
self.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if let strongSelf = self, let component = strongSelf.component {
|
||||||
|
if highlighted {
|
||||||
|
strongSelf.backgroundView.backgroundColor = component.theme.list.itemHighlightedBackgroundColor
|
||||||
|
} else {
|
||||||
|
UIView.animate(withDuration: 0.3, animations: {
|
||||||
|
strongSelf.backgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func pressed() {
|
||||||
|
if let component = self.component {
|
||||||
|
component.action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(component: TranslateButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
self.backgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor
|
||||||
|
self.backgroundView.layer.cornerRadius = 10.0
|
||||||
|
|
||||||
|
let _ = self.centralContentView.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(component.content),
|
||||||
|
environment: {},
|
||||||
|
containerSize: availableSize
|
||||||
|
)
|
||||||
|
transition.setFrame(view: self.centralContentView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
|
||||||
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
|
||||||
|
|
||||||
|
self.centralContentView.alpha = component.isEnabled ? 1.0 : 0.4
|
||||||
|
self.isUserInteractionEnabled = component.isEnabled
|
||||||
|
|
||||||
|
return availableSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
1109
submodules/TranslateUI/Sources/TranslateScreen.swift
Normal file
1109
submodules/TranslateUI/Sources/TranslateScreen.swift
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"app": "8.6",
|
"app": "8.6.1",
|
||||||
"bazel": "5.0.0",
|
"bazel": "5.0.0",
|
||||||
"xcode": "13.2.1"
|
"xcode": "13.2.1"
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user