Files
Swiftgram/submodules/TranslateUI/Sources/TranslateScreen.swift
Ilya Laktyushin 2ba1f6183e Various fixes
2026-02-03 15:27:06 +04:00

1505 lines
74 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import Speak
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import UndoUI
import SwiftUI
import ResizableSheetComponent
import GlassBarButtonComponent
private func generateExpandBackground(size: CGSize, color: UIColor) -> UIImage {
return generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
var locations: [CGFloat] = [0.0, 1.0]
let colors: [CGColor] = [color.withAlphaComponent(0.0).cgColor, color.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 40.0, y: size.height), options: CGGradientDrawingOptions())
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(x: 40.0, y: 0.0), size: CGSize(width: size.width - 40.0, height: size.height)))
})!
}
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let text: String
let entities: [MessageTextEntity]
let fromLanguage: String?
let toLanguage: String
let copyTranslation: ((String) -> Void)?
let changeLanguage: (String, String, @escaping (String, String) -> Void) -> Void
let expand: () -> Void
init(context: AccountContext, text: String, entities: [MessageTextEntity], fromLanguage: String?, toLanguage: String, copyTranslation: ((String) -> Void)?, changeLanguage: @escaping (String, String, @escaping (String, String) -> Void) -> Void, expand: @escaping () -> Void) {
self.context = context
self.text = text
self.entities = entities
self.fromLanguage = fromLanguage
self.toLanguage = toLanguage
self.copyTranslation = copyTranslation
self.changeLanguage = changeLanguage
self.expand = expand
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.entities != rhs.entities {
return false
}
if lhs.fromLanguage != rhs.fromLanguage {
return false
}
if lhs.toLanguage != rhs.toLanguage {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
var fromLanguage: String?
let text: String
var textExpanded: Bool = false
var toLanguage: String
var translatedText: String?
private let expand: () -> Void
private var translationDisposable = MetaDisposable()
fileprivate var isSpeakingOriginalText: Bool = false
fileprivate var isSpeakingTranslatedText: Bool = false
private var speechHolder: SpeechSynthesizerHolder?
fileprivate var availableSpeakLanguages: Set<String>
fileprivate var moreBackgroundImage: (CGSize, UIImage, UIColor)?
private let useAlternativeTranslation: Bool
init(context: AccountContext, fromLanguage: String?, text: String, toLanguage: String, expand: @escaping () -> Void) {
self.context = context
self.text = text
self.fromLanguage = fromLanguage
self.toLanguage = toLanguage
self.expand = expand
self.availableSpeakLanguages = supportedSpeakLanguages()
let translationConfiguration = TranslationConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
var useAlternativeTranslation = false
switch translationConfiguration.manual {
case .alternative:
useAlternativeTranslation = true
default:
break
}
self.useAlternativeTranslation = useAlternativeTranslation
super.init()
self.translationDisposable.set((self.translate(text: text, fromLang: fromLanguage, toLang: toLanguage) |> deliverOnMainQueue).start(next: { [weak self] text in
guard let strongSelf = self else {
return
}
strongSelf.translatedText = text?.0
strongSelf.updated(transition: .immediate)
}, error: { error in
}))
}
deinit {
self.speechHolder?.stop()
self.translationDisposable.dispose()
}
func translate(text: String, fromLang: String?, toLang: String) -> Signal<(String, [MessageTextEntity])?, TranslationError> {
if self.useAlternativeTranslation {
return alternativeTranslateText(text: text, fromLang: fromLang, toLang: toLang)
} else {
return self.context.engine.messages.translate(text: text, toLang: toLang)
}
}
func changeLanguage(fromLanguage: String, toLanguage: String) {
guard self.fromLanguage != fromLanguage || self.toLanguage != toLanguage else {
return
}
self.fromLanguage = fromLanguage
self.toLanguage = toLanguage
self.translatedText = nil
self.updated(transition: .immediate)
self.translationDisposable.set((self.translate(text: text, fromLang: fromLanguage, toLang: toLanguage) |> deliverOnMainQueue).start(next: { [weak self] text in
guard let strongSelf = self else {
return
}
strongSelf.translatedText = text?.0
strongSelf.updated(transition: .immediate)
}, error: { error in
}))
}
func expandText() {
self.textExpanded = true
self.updated(transition: .immediate)
self.expand()
}
func speakOriginalText() {
if let speechHolder = self.speechHolder {
self.speechHolder = nil
speechHolder.stop()
}
if self.isSpeakingOriginalText {
self.isSpeakingOriginalText = false
} else {
self.isSpeakingTranslatedText = false
self.isSpeakingOriginalText = true
self.speechHolder = speakText(context: self.context, text: self.text)
self.speechHolder?.completion = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isSpeakingOriginalText = false
strongSelf.updated(transition: .immediate)
}
}
self.updated(transition: .immediate)
}
func speakTranslatedText() {
guard let translatedText = self.translatedText else {
return
}
if let speechHolder = self.speechHolder {
self.speechHolder = nil
speechHolder.stop()
}
if self.isSpeakingTranslatedText {
self.isSpeakingTranslatedText = false
} else {
self.isSpeakingOriginalText = false
self.isSpeakingTranslatedText = true
self.speechHolder = speakText(context: self.context, text: translatedText)
self.speechHolder?.completion = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isSpeakingTranslatedText = false
strongSelf.updated(transition: .immediate)
}
}
self.updated(transition: .immediate)
}
}
func makeState() -> State {
return State(context: self.context, fromLanguage: self.fromLanguage, text: self.text, toLanguage: self.toLanguage, expand: self.expand)
}
static var body: Body {
let textBackground = Child(RoundedRectangle.self)
let originalTitle = Child(MultilineTextComponent.self)
let originalText = Child(MultilineTextComponent.self)
let originalMoreBackground = Child(Image.self)
let originalMoreButton = Child(Button.self)
let originalSpeakButton = Child(Button.self)
let translationTitle = Child(MultilineTextComponent.self)
let translationText = Child(MultilineTextComponent.self)
let translationPlaceholder = Child(RoundedRectangle.self)
let translationSpeakButton = Child(Button.self)
let copyButton = Child(TranslateButtonComponent.self)
let changeLanguageButton = Child(TranslateButtonComponent.self)
let textStripe = Child(Rectangle.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let state = context.state
let theme = environment.theme.withModalBlocksBackground()
let strings = environment.strings
let topInset: CGFloat = environment.navigationHeight - 35.0
let sideInset: CGFloat = 20.0 + environment.safeInsets.left
let textTopInset: CGFloat = 16.0
let textSideInset: CGFloat = 20.0
let textSpacing: CGFloat = 5.0
let itemSpacing: CGFloat = 20.0
let itemHeight: CGFloat = 52.0
var languageCode = environment.strings.baseLanguageCode
let rawSuffix = "-raw"
if languageCode.hasSuffix(rawSuffix) {
languageCode = String(languageCode.dropLast(rawSuffix.count))
}
let locale = Locale(identifier: languageCode)
let fromLanguage: String
if let languageCode = state.fromLanguage {
fromLanguage = locale.localizedString(forLanguageCode: languageCode) ?? ""
} else {
fromLanguage = ""
}
let originalTitle = originalTitle.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: fromLanguage, font: Font.medium(13.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)),
horizontalAlignment: .natural,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let originalText = originalText.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: state.text, font: Font.medium(20.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)),
horizontalAlignment: .natural,
maximumNumberOfLines: state.textExpanded ? 0 : 1,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0 - (state.textExpanded ? 30.0 : 0.0), height: context.availableSize.height),
transition: .immediate
)
let toLanguage = locale.localizedString(forLanguageCode: state.toLanguage) ?? ""
let translationTitle = translationTitle.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: toLanguage, font: Font.medium(13.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural)),
horizontalAlignment: .natural,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let translationTextHeight: CGFloat
var maybeTranslationText: _UpdatedChildComponent? = nil
var maybeTranslationPlaceholder: _UpdatedChildComponent? = nil
if let translatedText = state.translatedText {
maybeTranslationText = translationText.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: translatedText, font: Font.medium(20.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural)),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0 - 30.0, height: context.availableSize.height),
transition: .immediate
)
translationTextHeight = maybeTranslationText?.size.height ?? 0.0
} else {
maybeTranslationPlaceholder = translationPlaceholder.update(
component: RoundedRectangle(color: theme.list.itemAccentColor.withAlphaComponent(0.17), cornerRadius: 6.0),
availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0 - 42.0, height: 12.0),
transition: .immediate
)
translationTextHeight = 22.0
}
let textBackgroundOrigin = CGPoint(x: sideInset, y: topInset)
let textStripe = textStripe.update(
component: Rectangle(color: theme.list.itemPlainSeparatorColor),
availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0, height: UIScreenPixel),
transition: .immediate
)
let textBackgroundSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight + itemSpacing)
let textBackground = textBackground.update(
component: RoundedRectangle(color: theme.list.itemBlocksBackgroundColor, cornerRadius: 26.0),
availableSize: textBackgroundSize,
transition: context.transition
)
context.add(textBackground
.position(CGPoint(x: textBackgroundOrigin.x + textBackgroundSize.width / 2.0, y: topInset + textBackgroundSize.height / 2.0))
)
context.add(textStripe
.position(CGPoint(x: textBackgroundOrigin.x + textSideInset + textStripe.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing))
)
context.add(originalTitle
.position(CGPoint(x: textBackgroundOrigin.x + textSideInset + originalTitle.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height / 2.0))
)
context.add(originalText
.position(CGPoint(x: textBackgroundOrigin.x + textSideInset + originalText.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height / 2.0))
)
if state.textExpanded {
if let fromLanguage = state.fromLanguage, state.availableSpeakLanguages.contains(fromLanguage) {
var checkColor = theme.list.itemCheckColors.foregroundColor
if checkColor.rgb == theme.list.itemPrimaryTextColor.rgb {
checkColor = theme.list.plainBackgroundColor
}
let originalSpeakButton = originalSpeakButton.update(
component: Button(
content: AnyComponent(ZStack([
AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle(
fillColor: theme.list.itemPrimaryTextColor,
size: CGSize(width: 26.0, height: 26.0)
))),
AnyComponentWithIdentity(id: "a", component: AnyComponent(PlayPauseIconComponent(
state: state.isSpeakingOriginalText ? .pause : .play,
tintColor: checkColor,
size: CGSize(width: 20.0, height: 20.0)
))),
])),
action: { [weak state] in
guard let state = state else {
return
}
state.speakOriginalText()
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 26.0, height: 26.0),
transition: .immediate
)
context.add(originalSpeakButton
.position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - originalSpeakButton.size.width / 2.0 + 9.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height - originalSpeakButton.size.height / 2.0 - 2.0 + 12.0))
)
}
} else {
let originalMoreButton = originalMoreButton.update(
component: Button(
content: AnyComponent(Text(text: strings.PeerInfo_BioExpand, font: Font.regular(17.0), color: theme.list.itemAccentColor)),
action: { [weak state] in
guard let state = state else {
return
}
state.expandText()
}
),
availableSize: context.availableSize,
transition: .immediate
)
let originalMoreBackgroundSize = CGSize(width: originalMoreButton.size.width + 50.0, height: originalMoreButton.size.height)
let originalMoreBackgroundImage: UIImage
let backgroundColor = theme.list.itemBlocksBackgroundColor
if let (size, image, color) = state.moreBackgroundImage, size == originalMoreBackgroundSize && color == backgroundColor {
originalMoreBackgroundImage = image
} else {
originalMoreBackgroundImage = generateExpandBackground(size: originalMoreBackgroundSize, color: backgroundColor)
state.moreBackgroundImage = (originalMoreBackgroundSize, originalMoreBackgroundImage, backgroundColor)
}
let originalMoreBackground = originalMoreBackground.update(
component: Image(image: originalMoreBackgroundImage, tintColor: backgroundColor),
availableSize: originalMoreBackgroundSize,
transition: .immediate
)
context.add(originalMoreBackground
.position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - originalMoreBackground.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalMoreBackground.size.height / 2.0 - 1.0))
)
context.add(originalMoreButton
.position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - originalMoreButton.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height / 2.0 - 1.0))
)
}
context.add(translationTitle
.position(CGPoint(x: textBackgroundOrigin.x + textSideInset + translationTitle.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height / 2.0))
)
if let translationText = maybeTranslationText {
context.add(translationText
.position(CGPoint(x: textBackgroundOrigin.x + textSideInset + translationText.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationText.size.height / 2.0))
)
if state.availableSpeakLanguages.contains(state.toLanguage) {
let translationSpeakButton = translationSpeakButton.update(
component: Button(
content: AnyComponent(ZStack([
AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle(
fillColor: theme.list.itemAccentColor,
size: CGSize(width: 26.0, height: 26.0)
))),
AnyComponentWithIdentity(id: "a", component: AnyComponent(PlayPauseIconComponent(
state: state.isSpeakingTranslatedText ? .pause : .play,
tintColor: theme.list.itemCheckColors.foregroundColor,
size: CGSize(width: 20.0, height: 20.0)
))),
])),
action: { [weak state] in
guard let state = state else {
return
}
state.speakTranslatedText()
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 26.0, height: 26.0),
transition: .immediate
)
context.add(translationSpeakButton
.position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - translationSpeakButton.size.width / 2.0 + 9.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight - translationSpeakButton.size.height / 2.0 - 2.0 + 12.0))
.appear(.default())
.disappear(.default())
)
}
} else if let translationPlaceholder = maybeTranslationPlaceholder {
context.add(translationPlaceholder
.position(CGPoint(x: textBackgroundOrigin.x + textSideInset + translationPlaceholder.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationPlaceholder.size.height / 2.0 + 4.0))
)
}
let buttonsSpacing: CGFloat = 20.0
let smallSectionSpacing: CGFloat = 20.0
var buttonsHeight: CGFloat = 0.0
let component = context.component
if component.copyTranslation != nil {
let copyButton = copyButton.update(
component: TranslateButtonComponent(
theme: theme,
title: strings.Translate_CopyTranslation,
icon: "Chat/Context Menu/Copy",
isEnabled: state.translatedText != nil,
action: { [weak component] in
component?.copyTranslation?(state.translatedText ?? "")
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: itemHeight),
transition: context.transition
)
context.add(copyButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight + itemSpacing + buttonsSpacing + copyButton.size.height / 2.0))
)
buttonsHeight += copyButton.size.height + smallSectionSpacing
}
let changeLanguageButton = changeLanguageButton.update(
component: TranslateButtonComponent(
theme: theme,
title: strings.Translate_ChangeLanguage,
icon: "Chat/Context Menu/Translate",
isEnabled: true,
action: { [weak component] in
component?.changeLanguage(state.fromLanguage ?? "", state.toLanguage, { fromLang, toLang in
state.changeLanguage(fromLanguage: fromLang, toLanguage: toLang)
})
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: itemHeight),
transition: context.transition
)
context.add(changeLanguageButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight + itemSpacing + buttonsSpacing + buttonsHeight + changeLanguageButton.size.height / 2.0))
)
buttonsHeight += changeLanguageButton.size.height
let contentSize = CGSize(width: context.availableSize.width, height: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight + itemSpacing + buttonsSpacing + buttonsHeight + environment.safeInsets.bottom + 44.0)
return contentSize
}
}
}
private final class TranslateSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
private let context: AccountContext
private let text: String
private let entities: [MessageTextEntity]
private let fromLanguage: String?
private let toLanguage: String
private let copyTranslation: ((String) -> Void)?
private let changeLanguage: (String, String, @escaping (String, String) -> Void) -> Void
init(
context: AccountContext,
text: String,
entities: [MessageTextEntity],
fromLanguage: String?,
toLanguage: String,
copyTranslation: ((String) -> Void)?,
changeLanguage: @escaping (String, String, @escaping (String, String) -> Void) -> Void
) {
self.context = context
self.text = text
self.entities = entities
self.fromLanguage = fromLanguage
self.toLanguage = toLanguage
self.copyTranslation = copyTranslation
self.changeLanguage = changeLanguage
}
static func ==(lhs: TranslateSheetComponent, rhs: TranslateSheetComponent) -> Bool {
return true
}
static var body: Body {
let sheet = Child(ResizableSheetComponent<(EnvironmentType)>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let dismiss: (Bool) -> Void = { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
let theme = environment.theme.withModalBlocksBackground()
let sheet = sheet.update(
component: ResizableSheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
text: context.component.text,
entities: context.component.entities,
fromLanguage: context.component.fromLanguage,
toLanguage: context.component.toLanguage,
copyTranslation: context.component.copyTranslation,
changeLanguage: context.component.changeLanguage,
expand: {}
)),
titleItem: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Translate_Title, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)))
),
leftItem: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
dismiss(true)
}
)
),
hasTopEdgeEffect: false,
bottomItem: nil,
backgroundColor: .color(theme.actionSheet.opaqueItemBackgroundColor),
animateOut: animateOut
),
environment: {
environment
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environment.statusBarHeight,
safeInsets: environment.safeInsets,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
screenSize: context.availableSize,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
dismiss(animated)
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public final class TranslateScreen: ViewControllerComponentContainer {
private let context: AccountContext
public var pushController: (ViewController) -> Void = { _ in }
public var presentController: (ViewController) -> Void = { _ in }
public init(
context: AccountContext,
forceTheme: PresentationTheme? = nil,
text: String,
entities: [MessageTextEntity] = [],
canCopy: Bool,
fromLanguage: String?,
toLanguage: String? = nil,
ignoredLanguages: [String]? = nil
) {
self.context = context
let theme: ViewControllerComponentContainer.Theme
if let forceTheme {
theme = .custom(forceTheme)
} else {
theme = .default
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var baseLanguageCode = presentationData.strings.baseLanguageCode
let rawSuffix = "-raw"
if baseLanguageCode.hasSuffix(rawSuffix) {
baseLanguageCode = String(baseLanguageCode.dropLast(rawSuffix.count))
}
let dontTranslateLanguages = effectiveIgnoredTranslationLanguages(context: context, ignoredLanguages: ignoredLanguages)
var toLanguage = toLanguage ?? baseLanguageCode
if toLanguage == fromLanguage {
if fromLanguage == "en" {
toLanguage = dontTranslateLanguages.first(where: { $0 != "en" }) ?? "en"
} else {
toLanguage = "en"
}
}
toLanguage = normalizeTranslationLanguage(toLanguage)
var copyTranslationImpl: ((String) -> Void)?
var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)?
super.init(
context: context,
component: TranslateSheetComponent(
context: context,
text: text,
entities: entities,
fromLanguage: fromLanguage,
toLanguage: toLanguage,
copyTranslation: !canCopy ? nil : { text in
copyTranslationImpl?(text)
},
changeLanguage: { fromLang, toLang, completion in
changeLanguageImpl?(fromLang, toLang, completion)
}
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: theme
)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
copyTranslationImpl = { [weak self] text in
UIPasteboard.general.string = text
let content = UndoOverlayContent.copy(text: presentationData.strings.Conversation_TextCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
self?.dismiss(animated: true, completion: nil)
}
changeLanguageImpl = { [weak self] fromLang, toLang, completion in
let pushController = self?.pushController
let presentController = self?.presentController
let controller = languageSelectionController(context: context, forceTheme: forceTheme, fromLanguage: fromLang, toLanguage: toLang, completion: { fromLang, toLang in
let controller = TranslateScreen(context: context, forceTheme: forceTheme, text: text, canCopy: canCopy, fromLanguage: fromLang, toLanguage: toLang, ignoredLanguages: ignoredLanguages)
controller.pushController = pushController ?? { _ in }
controller.presentController = presentController ?? { _ in }
presentController?(controller)
})
self?.dismissAnimated()
pushController?(controller)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
//public class TranslateScreen: ViewController {
// final class Node: ViewControllerTracingNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
// private var presentationData: PresentationData
// private weak var controller: TranslateScreen?
//
// private let component: AnyComponent<ViewControllerComponentContainer.Environment>
// private let theme: PresentationTheme?
//
// let dim: ASDisplayNode
// let wrappingView: UIView
// let containerView: UIView
// let scrollView: UIScrollView
// let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
//
// private(set) var isExpanded = false
// private var panGestureRecognizer: UIPanGestureRecognizer?
// private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)?
//
// private var currentIsVisible: Bool = false
// private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
//
// fileprivate var temporaryDismiss = false
//
// init(context: AccountContext, controller: TranslateScreen, component: AnyComponent<ViewControllerComponentContainer.Environment>, theme: PresentationTheme?) {
// self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
//
// self.controller = controller
//
// self.component = component
// self.theme = theme
//
// let effectiveTheme = theme ?? self.presentationData.theme
//
// self.dim = ASDisplayNode()
// self.dim.alpha = 0.0
// self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
//
// self.wrappingView = UIView()
// self.containerView = UIView()
// self.scrollView = UIScrollView()
// self.hostView = ComponentHostView()
//
// super.init()
//
// self.scrollView.delegate = self.wrappedScrollViewDelegate
// self.scrollView.showsVerticalScrollIndicator = false
//
// self.containerView.clipsToBounds = true
// self.containerView.backgroundColor = effectiveTheme.list.blocksBackgroundColor
//
// self.addSubnode(self.dim)
//
// self.view.addSubview(self.wrappingView)
// self.wrappingView.addSubview(self.containerView)
// self.containerView.addSubview(self.scrollView)
// self.scrollView.addSubview(self.hostView)
// }
//
// override func didLoad() {
// super.didLoad()
//
// let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
// panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
// panRecognizer.delaysTouchesBegan = false
// panRecognizer.cancelsTouchesInView = true
// self.panGestureRecognizer = panRecognizer
// self.wrappingView.addGestureRecognizer(panRecognizer)
//
// self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
//
// self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
// }
//
// @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
// if case .ended = recognizer.state {
// self.controller?.dismiss(animated: true)
// }
// }
//
// override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// if let (layout, _) = self.currentLayout {
// if case .regular = layout.metrics.widthClass {
// return false
// }
// }
// return true
// }
//
// func scrollViewDidScroll(_ scrollView: UIScrollView) {
// let contentOffset = self.scrollView.contentOffset.y
// self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate)
// }
//
// func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
// return true
// }
// return false
// }
//
// private var isDismissing = false
// func animateIn() {
// ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
//
// let targetPosition = self.containerView.center
// let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height)
//
// self.containerView.center = startPosition
// let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
// transition.animateView(allowUserInteraction: true, {
// self.containerView.center = targetPosition
// }, completion: { _ in
// })
// }
//
// func animateOut(completion: @escaping () -> Void = {}) {
// self.isDismissing = true
//
// let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
// positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in
// self?.controller?.dismiss(animated: false, completion: completion)
// })
// let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
// alphaTransition.updateAlpha(node: self.dim, alpha: 0.0)
//
// if !self.temporaryDismiss {
// self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition)
// }
// }
//
// func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) {
// self.currentLayout = (layout, navigationHeight)
//
// if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView {
// self.containerView.addSubview(navigationBar.view)
// }
//
// self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0))
//
// var effectiveExpanded = self.isExpanded
// if case .regular = layout.metrics.widthClass {
// effectiveExpanded = true
// }
//
// let isLandscape = layout.orientation == .landscape
// let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset
// let topInset: CGFloat
// if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments {
// if effectiveExpanded {
// topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset))
// } else {
// topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
// }
// } else {
// topInset = effectiveExpanded ? 0.0 : edgeTopInset
// }
// transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil)
//
// let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset)
// self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition)
//
// let clipFrame: CGRect
// if layout.metrics.widthClass == .compact {
// self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25)
// if isLandscape {
// self.containerView.layer.cornerRadius = 0.0
// } else {
// self.containerView.layer.cornerRadius = 10.0
// }
//
// if #available(iOS 11.0, *) {
// if layout.safeInsets.bottom.isZero {
// self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
// } else {
// self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
// }
// }
//
// if isLandscape {
// clipFrame = CGRect(origin: CGPoint(), size: layout.size)
// } else {
// let coveredByModalTransition: CGFloat = 0.0
// var containerTopInset: CGFloat = 10.0
// if let statusBarHeight = layout.statusBarHeight {
// containerTopInset += statusBarHeight
// }
//
// let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset))
// let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width
// let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
// let maxScaledTopInset: CGFloat = containerTopInset - 10.0
// let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
// let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
//
// clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height)
// }
// } else {
// self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
// self.containerView.layer.cornerRadius = 10.0
//
// let verticalInset: CGFloat = 44.0
//
// let maxSide = max(layout.size.width, layout.size.height)
// let minSide = min(layout.size.width, layout.size.height)
// let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
// clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
// }
//
// transition.setFrame(view: self.containerView, frame: clipFrame)
// transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil)
//
// let environment = ViewControllerComponentContainer.Environment(
// statusBarHeight: 0.0,
// navigationHeight: navigationHeight,
// safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right),
// additionalInsets: layout.additionalInsets,
// inputHeight: layout.inputHeight ?? 0.0,
// metrics: layout.metrics,
// deviceMetrics: layout.deviceMetrics,
// orientation: layout.metrics.orientation,
// isVisible: self.currentIsVisible,
// theme: self.theme ?? self.presentationData.theme,
// strings: self.presentationData.strings,
// dateTimeFormat: self.presentationData.dateTimeFormat,
// controller: { [weak self] in
// return self?.controller
// }
// )
// var contentSize = self.hostView.update(
// transition: transition,
// component: self.component,
// environment: {
// environment
// },
// forceUpdate: true,
// containerSize: CGSize(width: clipFrame.size.width, height: 10000.0)
// )
// contentSize.height = max(layout.size.height - navigationHeight, contentSize.height)
// transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil)
//
// self.scrollView.contentSize = contentSize
// }
//
// private var didPlayAppearAnimation = false
// func updateIsVisible(isVisible: Bool) {
// if self.currentIsVisible == isVisible {
// return
// }
// self.currentIsVisible = isVisible
//
// guard let currentLayout = self.currentLayout else {
// return
// }
// self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate)
//
// if !self.didPlayAppearAnimation {
// self.didPlayAppearAnimation = true
// self.animateIn()
// }
// }
//
// private var defaultTopInset: CGFloat {
// guard let (layout, _) = self.currentLayout else{
// return 210.0
// }
// if case .compact = layout.metrics.widthClass {
// var factor: CGFloat = 0.2488
// if layout.size.width <= 320.0 {
// factor = 0.15
// }
// return floor(max(layout.size.width, layout.size.height) * factor)
// } else {
// return 210.0
// }
// }
//
// private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? {
// if let view = view {
// if let view = view as? UIScrollView {
// return (view, nil)
// }
// if let node = view.asyncdisplaykit_node as? ListView {
// return (node.scroller, node)
// }
// return findScrollView(view: view.superview)
// } else {
// return nil
// }
// }
//
// @objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
// guard let (layout, navigationHeight) = self.currentLayout else {
// return
// }
//
// let isLandscape = layout.orientation == .landscape
// let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
//
// switch recognizer.state {
// case .began:
// let point = recognizer.location(in: self.view)
// let currentHitView = self.hitTest(point, with: nil)
//
// var scrollViewAndListNode = self.findScrollView(view: currentHitView)
// if scrollViewAndListNode?.0.frame.height == self.frame.width {
// scrollViewAndListNode = nil
// }
// let scrollView = scrollViewAndListNode?.0
// let listNode = scrollViewAndListNode?.1
//
// let topInset: CGFloat
// if self.isExpanded {
// topInset = 0.0
// } else {
// topInset = edgeTopInset
// }
//
// self.panGestureArguments = (topInset, 0.0, scrollView, listNode)
// case .changed:
// guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
// return
// }
// let visibleContentOffset = listNode?.visibleContentOffset()
// let contentOffset = scrollView?.contentOffset.y ?? 0.0
//
// var translation = recognizer.translation(in: self.view).y
//
// var currentOffset = topInset + translation
//
// let epsilon = 1.0
// if case let .known(value) = visibleContentOffset, value <= epsilon {
// if let scrollView = scrollView {
// scrollView.bounces = false
// scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false)
// }
// } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon {
// scrollView.bounces = false
// scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
// } else if let scrollView = scrollView {
// translation = panOffset
// currentOffset = topInset + translation
// if self.isExpanded {
// recognizer.setTranslation(CGPoint(), in: self.view)
// } else if currentOffset > 0.0 {
// scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
// }
// }
//
// self.panGestureArguments = (topInset, translation, scrollView, listNode)
//
// if !self.isExpanded {
// if currentOffset > 0.0, let scrollView = scrollView {
// scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView)
// }
// }
//
// var bounds = self.bounds
// if self.isExpanded {
// bounds.origin.y = -max(0.0, translation - edgeTopInset)
// } else {
// bounds.origin.y = -translation
// }
// bounds.origin.y = min(0.0, bounds.origin.y)
// self.bounds = bounds
//
// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
// case .ended:
// guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
// return
// }
// self.panGestureArguments = nil
//
// let visibleContentOffset = listNode?.visibleContentOffset()
// let contentOffset = scrollView?.contentOffset.y ?? 0.0
//
// let translation = recognizer.translation(in: self.view).y
// var velocity = recognizer.velocity(in: self.view)
//
// if self.isExpanded {
// if case let .known(value) = visibleContentOffset, value > 0.1 {
// velocity = CGPoint()
// } else if case .unknown = visibleContentOffset {
// velocity = CGPoint()
// } else if contentOffset > 0.1 {
// velocity = CGPoint()
// }
// }
//
// var bounds = self.bounds
// if self.isExpanded {
// bounds.origin.y = -max(0.0, translation - edgeTopInset)
// } else {
// bounds.origin.y = -translation
// }
// bounds.origin.y = min(0.0, bounds.origin.y)
//
// scrollView?.bounces = true
//
// let offset = currentTopInset + panOffset
// let topInset: CGFloat = edgeTopInset
//
// var dismissing = false
// if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) {
// self.controller?.dismiss(animated: true, completion: nil)
// dismissing = true
// } else if self.isExpanded {
// if velocity.y > 300.0 || offset > topInset / 2.0 {
// self.isExpanded = false
// if let listNode = listNode {
// listNode.scroller.setContentOffset(CGPoint(), animated: false)
// } else if let scrollView = scrollView {
// scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
// }
//
// let distance = topInset - offset
// let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance)
// let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
//
// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
// } else {
// self.isExpanded = true
//
// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut)))
// }
// } else if (velocity.y < -300.0 || offset < topInset / 2.0) {
// if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode {
// DispatchQueue.main.async {
// listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
// }
// }
//
// let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset)
// let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
// self.isExpanded = true
//
// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
// } else {
// if let listNode = listNode {
// listNode.scroller.setContentOffset(CGPoint(), animated: false)
// } else if let scrollView = scrollView {
// scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
// }
//
// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut)))
// }
//
// if !dismissing {
// var bounds = self.bounds
// let previousBounds = bounds
// bounds.origin.y = 0.0
// self.bounds = bounds
// self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
// }
// case .cancelled:
// self.panGestureArguments = nil
//
// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut)))
// default:
// break
// }
// }
//
// func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) {
// guard isExpanded != self.isExpanded else {
// return
// }
// self.isExpanded = isExpanded
//
// guard let (layout, navigationHeight) = self.currentLayout else {
// return
// }
// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
// }
// }
//
// var node: Node {
// return self.displayNode as! Node
// }
//
// private let context: AccountContext
// private let theme: PresentationTheme?
// private let component: AnyComponent<ViewControllerComponentContainer.Environment>
// private var isInitiallyExpanded = false
//
// private var currentLayout: ContainerViewLayout?
//
// public var pushController: (ViewController) -> Void = { _ in }
// public var presentController: (ViewController) -> Void = { _ in }
//
// public var wasDismissed: (() -> Void)?
//
// public convenience init(context: AccountContext, forceTheme: PresentationTheme? = nil, text: String, entities: [MessageTextEntity] = [], canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false, ignoredLanguages: [String]? = nil) {
// let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//
// var baseLanguageCode = presentationData.strings.baseLanguageCode
// let rawSuffix = "-raw"
// if baseLanguageCode.hasSuffix(rawSuffix) {
// baseLanguageCode = String(baseLanguageCode.dropLast(rawSuffix.count))
// }
//
// let dontTranslateLanguages = effectiveIgnoredTranslationLanguages(context: context, ignoredLanguages: ignoredLanguages)
//
// var toLanguage = toLanguage ?? baseLanguageCode
// if toLanguage == fromLanguage {
// if fromLanguage == "en" {
// toLanguage = dontTranslateLanguages.first(where: { $0 != "en" }) ?? "en"
// } else {
// toLanguage = "en"
// }
// }
//
// toLanguage = normalizeTranslationLanguage(toLanguage)
//
// var copyTranslationImpl: ((String) -> Void)?
// var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)?
// var expandImpl: (() -> Void)?
// self.init(context: context, component: TranslateScreenComponent(context: context, text: text, entities: entities, fromLanguage: fromLanguage, toLanguage: toLanguage, copyTranslation: !canCopy ? nil : { text in
// copyTranslationImpl?(text)
// }, changeLanguage: { fromLang, toLang, completion in
// changeLanguageImpl?(fromLang, toLang, completion)
// }, expand: {
// expandImpl?()
// }), theme: forceTheme)
//
// self.isInitiallyExpanded = isExpanded
//
// self.title = presentationData.strings.Translate_Title
//
// self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
//
// self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
//
// copyTranslationImpl = { [weak self] text in
// UIPasteboard.general.string = text
// let content = UndoOverlayContent.copy(text: presentationData.strings.Conversation_TextCopied)
// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
// self?.dismiss(animated: true, completion: nil)
// }
//
// changeLanguageImpl = { [weak self] fromLang, toLang, completion in
// let pushController = self?.pushController
// let presentController = self?.presentController
// let controller = languageSelectionController(context: context, forceTheme: forceTheme, fromLanguage: fromLang, toLanguage: toLang, completion: { fromLang, toLang in
// let controller = TranslateScreen(context: context, forceTheme: forceTheme, text: text, canCopy: canCopy, fromLanguage: fromLang, toLanguage: toLang, isExpanded: true, ignoredLanguages: ignoredLanguages)
// controller.pushController = pushController ?? { _ in }
// controller.presentController = presentController ?? { _ in }
// presentController?(controller)
// })
//
// self?.node.temporaryDismiss = true
// self?.dismiss(animated: true, completion: nil)
//
// pushController?(controller)
// }
//
// expandImpl = { [weak self] in
// self?.node.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
// if let currentLayout = self?.currentLayout {
// self?.containerLayoutUpdated(currentLayout, transition: .animated(duration: 0.4, curve: .spring))
// }
// }
// }
//
// private init<C: Component>(context: AccountContext, component: C, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
// self.context = context
// self.component = AnyComponent(component)
// self.theme = theme
//
// var presentationData = context.sharedContext.currentPresentationData.with { $0 }
// if let theme {
// presentationData = presentationData.withUpdated(theme: theme)
// }
// super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData))
// }
//
// required public init(coder aDecoder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
// }
//
// @objc private func cancelPressed() {
// self.dismiss(animated: true, completion: nil)
// }
//
// override open func loadDisplayNode() {
// self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme)
// if self.isInitiallyExpanded {
// (self.displayNode as! Node).update(isExpanded: true, transition: .immediate)
// }
// self.displayNodeDidLoad()
// }
//
// public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// self.view.endEditing(true)
// let wasDismissed = self.wasDismissed
// if flag {
// self.node.animateOut(completion: {
// super.dismiss(animated: false, completion: {})
// wasDismissed?()
// completion?()
// })
// } else {
// super.dismiss(animated: false, completion: {})
// wasDismissed?()
// completion?()
// }
// }
//
// override open func viewDidAppear(_ animated: Bool) {
// super.viewDidAppear(animated)
//
// self.node.updateIsVisible(isVisible: true)
// }
//
// override open func viewDidDisappear(_ animated: Bool) {
// super.viewDidDisappear(animated)
//
// self.node.updateIsVisible(isVisible: false)
// }
//
// override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
// var navigationLayout = self.navigationLayout(layout: layout)
// var navigationFrame = navigationLayout.navigationFrame
//
// var layout = layout
// if case .regular = layout.metrics.widthClass {
// let verticalInset: CGFloat = 44.0
// let maxSide = max(layout.size.width, layout.size.height)
// let minSide = min(layout.size.width, layout.size.height)
// let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
// let clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
// navigationFrame.size.width = clipFrame.width
// layout.size = clipFrame.size
// }
//
// navigationFrame.size.height = 56.0
// navigationLayout.navigationFrame = navigationFrame
// navigationLayout.defaultContentHeight = 56.0
//
// layout.statusBarHeight = nil
//
// self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, additionalCutout: nil, transition: transition)
// }
//
// override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
// self.currentLayout = layout
// super.containerLayoutUpdated(layout, transition: transition)
//
// let navigationHeight: CGFloat = 56.0
//
// self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
// }
//}
public func presentTranslateScreen(
context: AccountContext,
text: String,
entities: [MessageTextEntity] = [],
canCopy: Bool,
fromLanguage: String?,
toLanguage: String? = nil,
isExpanded: Bool = false,
ignoredLanguages: [String]? = nil,
pushController: @escaping (ViewController) -> Void = { _ in },
presentController: @escaping (ViewController) -> Void = { _ in },
wasDismissed: (() -> Void)? = nil,
display: (ViewController) -> Void
) {
let translationConfiguration = TranslationConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
var useSystemTranslation = false
switch translationConfiguration.manual {
case .system:
if #available(iOS 18.0, *) {
useSystemTranslation = true
}
default:
break
}
if useSystemTranslation {
presentSystemTranslateScreen(context: context, text: text)
} else {
let controller = TranslateScreen(context: context, text: text, canCopy: canCopy, fromLanguage: fromLanguage, toLanguage: toLanguage, ignoredLanguages: ignoredLanguages)
controller.pushController = pushController
controller.presentController = presentController
controller.wasDismissed = wasDismissed
display(controller)
}
}
private func presentSystemTranslateScreen(context: AccountContext, text: String) {
if #available(iOS 18.0, *), let rootViewController = context.sharedContext.mainWindow?.viewController?.view.window?.rootViewController {
var dismissImpl: (() -> Void)?
let pickerView = TranslateScreenHostingView(text: text, completionHandler: { [weak rootViewController] in
DispatchQueue.main.async(execute: {
guard let presentedController = rootViewController?.presentedViewController, presentedController.isBeingDismissed == false else { return }
dismissImpl?()
})
})
let hostingController = UIHostingController(rootView: pickerView)
hostingController.view.isHidden = true
hostingController.modalPresentationStyle = .overCurrentContext
rootViewController.present(hostingController, animated: true)
dismissImpl = { [weak hostingController] in
Queue.mainQueue().after(0.4, {
hostingController?.dismiss(animated: false)
})
}
}
}
@available(iOS 18.0, *)
struct TranslateScreenHostingView: View {
@State var presented = true
var text: String
var handler: () -> Void
init(text: String, completionHandler: @escaping () -> Void) {
self.text = text
self.handler = completionHandler
}
var body: some View {
Spacer()
.translationPresentation(
isPresented: $presented,
text: text
)
.onChange(of: presented) { newValue in
if newValue == false {
handler()
}
}
}
}