mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Browser improvements
This commit is contained in:
parent
af5408d526
commit
345616704a
@ -12552,3 +12552,29 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"Stars.Gift.Sent.Title" = "Sent Gift";
|
"Stars.Gift.Sent.Title" = "Sent Gift";
|
||||||
"Stars.Gift.Sent.Text" = "With Stars, %@ will be able to unlock content and services on Telegram. [See Examples >]()";
|
"Stars.Gift.Sent.Text" = "With Stars, %@ will be able to unlock content and services on Telegram. [See Examples >]()";
|
||||||
|
|
||||||
|
"WebBrowser.AddBookmark" = "Add Bookmark";
|
||||||
|
|
||||||
|
"WebBrowser.LinkAddedToBookmarks" = "Link added to [Bookmarks]() and **Saved Messages**.";
|
||||||
|
|
||||||
|
"WebBrowser.AddressBar.RecentlyVisited" = "RECENTLY VISITED";
|
||||||
|
"WebBrowser.AddressBar.RecentlyVisited.Clear" = "Clear";
|
||||||
|
|
||||||
|
"WebBrowser.AddressBar.Bookmarks" = "BOOKMARKS";
|
||||||
|
|
||||||
|
"WebBrowser.OpenLinksIn.Title" = "OPEN LINKS IN";
|
||||||
|
"WebBrowser.AutoLogin" = "Auto-Login via Telegram";
|
||||||
|
"WebBrowser.AutoLogin.Info" = "Use your Telegram account to automatically log in to websites opened in the in-app browser.";
|
||||||
|
|
||||||
|
"WebBrowser.ClearCookies" = "Clear Cookies";
|
||||||
|
"WebBrowser.ClearCookies.Info" = "Delete all cookies in the Telegram in-app browser. This action will sign you out of most websites.";
|
||||||
|
"WebBrowser.ClearCookies.Succeed" = "Cookies cleared.";
|
||||||
|
|
||||||
|
"WebBrowser.Exceptions.Title" = "NEVER OPEN IN THE IN-APP BROWSER";
|
||||||
|
"WebBrowser.Exceptions.AddException" = "Add Website";
|
||||||
|
"WebBrowser.Exceptions.Clear" = "Clear List";
|
||||||
|
"WebBrowser.Exceptions.Info" = "These websites will be always opened in your default browser.";
|
||||||
|
|
||||||
|
"WebBrowser.Exceptions.Create.Title" = "Add Website";
|
||||||
|
"WebBrowser.Exceptions.Create.Text" = "Enter a domain that you don't want to be opened in the in-app browser.";
|
||||||
|
"WebBrowser.Exceptions.Create.Placeholder" = "Enter URL";
|
||||||
|
362
submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift
Normal file
362
submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AccountContext
|
||||||
|
import BundleIconComponent
|
||||||
|
|
||||||
|
final class AddressBarContentComponent: Component {
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let url: String
|
||||||
|
let performAction: ActionSlot<BrowserScreen.Action>
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
url: String,
|
||||||
|
performAction: ActionSlot<BrowserScreen.Action>
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.url = url
|
||||||
|
self.performAction = performAction
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: AddressBarContentComponent, rhs: AddressBarContentComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.url != rhs.url {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView, UITextFieldDelegate {
|
||||||
|
private final class TextField: UITextField {
|
||||||
|
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||||
|
return bounds.integral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Params: Equatable {
|
||||||
|
var theme: PresentationTheme
|
||||||
|
var strings: PresentationStrings
|
||||||
|
var size: CGSize
|
||||||
|
|
||||||
|
static func ==(lhs: Params, rhs: Params) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.size != rhs.size {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let activated: (Bool) -> Void = { _ in }
|
||||||
|
private let deactivated: (Bool) -> Void = { _ in }
|
||||||
|
private let updateQuery: (String?) -> Void = { _ in }
|
||||||
|
|
||||||
|
private let backgroundLayer: SimpleLayer
|
||||||
|
|
||||||
|
private let iconView: UIImageView
|
||||||
|
|
||||||
|
private let clearIconView: UIImageView
|
||||||
|
private let clearIconButton: HighlightTrackingButton
|
||||||
|
|
||||||
|
private let cancelButtonTitle: ComponentView<Empty>
|
||||||
|
private let cancelButton: HighlightTrackingButton
|
||||||
|
|
||||||
|
private var placeholderContent = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private var textFrame: CGRect?
|
||||||
|
private var textField: TextField?
|
||||||
|
|
||||||
|
private var tapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
|
private var params: Params?
|
||||||
|
private var component: AddressBarContentComponent?
|
||||||
|
|
||||||
|
public var wantsDisplayBelowKeyboard: Bool {
|
||||||
|
return self.textField != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.backgroundLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.iconView = UIImageView()
|
||||||
|
|
||||||
|
self.clearIconView = UIImageView()
|
||||||
|
self.clearIconButton = HighlightableButton()
|
||||||
|
self.clearIconView.isHidden = true
|
||||||
|
self.clearIconButton.isHidden = true
|
||||||
|
|
||||||
|
self.cancelButtonTitle = ComponentView()
|
||||||
|
self.cancelButton = HighlightTrackingButton()
|
||||||
|
|
||||||
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.backgroundLayer)
|
||||||
|
|
||||||
|
self.addSubview(self.iconView)
|
||||||
|
self.addSubview(self.clearIconView)
|
||||||
|
self.addSubview(self.clearIconButton)
|
||||||
|
|
||||||
|
self.addSubview(self.cancelButton)
|
||||||
|
self.clipsToBounds = true
|
||||||
|
|
||||||
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||||
|
self.tapRecognizer = tapRecognizer
|
||||||
|
self.addGestureRecognizer(tapRecognizer)
|
||||||
|
|
||||||
|
self.cancelButton.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if highlighted {
|
||||||
|
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
|
||||||
|
cancelButtonTitleView.layer.removeAnimation(forKey: "opacity")
|
||||||
|
cancelButtonTitleView.alpha = 0.4
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
|
||||||
|
cancelButtonTitleView.alpha = 1.0
|
||||||
|
cancelButtonTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
self.clearIconButton.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if highlighted {
|
||||||
|
strongSelf.clearIconView.layer.removeAnimation(forKey: "opacity")
|
||||||
|
strongSelf.clearIconView.alpha = 0.4
|
||||||
|
} else {
|
||||||
|
strongSelf.clearIconView.alpha = 1.0
|
||||||
|
strongSelf.clearIconView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.clearIconButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
self.activateTextInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func activateTextInput() {
|
||||||
|
if self.textField == nil, let textFrame = self.textFrame {
|
||||||
|
let backgroundFrame = self.backgroundLayer.frame
|
||||||
|
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
|
||||||
|
|
||||||
|
let textField = TextField(frame: textFieldFrame)
|
||||||
|
textField.autocorrectionType = .no
|
||||||
|
textField.returnKeyType = .search
|
||||||
|
self.textField = textField
|
||||||
|
self.insertSubview(textField, belowSubview: self.clearIconView)
|
||||||
|
textField.delegate = self
|
||||||
|
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !(self.textField?.isFirstResponder ?? false) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.activated(true)
|
||||||
|
|
||||||
|
self.textField?.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func cancelPressed() {
|
||||||
|
self.updateQuery(nil)
|
||||||
|
|
||||||
|
self.clearIconView.isHidden = true
|
||||||
|
self.clearIconButton.isHidden = true
|
||||||
|
|
||||||
|
let textField = self.textField
|
||||||
|
self.textField = nil
|
||||||
|
|
||||||
|
self.deactivated(textField?.isFirstResponder ?? false)
|
||||||
|
|
||||||
|
self.component?.performAction.invoke(.updateSearchActive(false))
|
||||||
|
|
||||||
|
if let textField {
|
||||||
|
textField.resignFirstResponder()
|
||||||
|
textField.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func clearPressed() {
|
||||||
|
self.updateQuery(nil)
|
||||||
|
self.textField?.text = ""
|
||||||
|
|
||||||
|
self.clearIconView.isHidden = true
|
||||||
|
self.clearIconButton.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func deactivate() {
|
||||||
|
if let text = self.textField?.text, !text.isEmpty {
|
||||||
|
self.textField?.endEditing(true)
|
||||||
|
} else {
|
||||||
|
self.cancelPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
|
textField.endEditing(true)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func textFieldChanged(_ textField: UITextField) {
|
||||||
|
let text = textField.text ?? ""
|
||||||
|
|
||||||
|
self.clearIconView.isHidden = text.isEmpty
|
||||||
|
self.clearIconButton.isHidden = text.isEmpty
|
||||||
|
self.placeholderContent.view?.isHidden = !text.isEmpty
|
||||||
|
|
||||||
|
self.updateQuery(text)
|
||||||
|
|
||||||
|
self.component?.performAction.invoke(.updateSearchQuery(text))
|
||||||
|
|
||||||
|
if let params = self.params {
|
||||||
|
self.update(theme: params.theme, strings: params.strings, size: params.size, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: AddressBarContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
self.update(theme: component.theme, strings: component.strings, size: availableSize, transition: transition)
|
||||||
|
|
||||||
|
return availableSize
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ComponentTransition) {
|
||||||
|
let params = Params(
|
||||||
|
theme: theme,
|
||||||
|
strings: strings,
|
||||||
|
size: size
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.params == params {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isActiveWithText = true
|
||||||
|
|
||||||
|
if self.params?.theme !== theme {
|
||||||
|
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||||
|
self.iconView.tintColor = theme.rootController.navigationSearchBar.inputIconColor
|
||||||
|
self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||||
|
self.clearIconView.tintColor = theme.rootController.navigationSearchBar.inputClearButtonColor
|
||||||
|
}
|
||||||
|
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 10.0
|
||||||
|
let inputHeight: CGFloat = 36.0
|
||||||
|
let topInset: CGFloat = (size.height - inputHeight) / 2.0
|
||||||
|
|
||||||
|
let sideTextInset: CGFloat = sideInset + 4.0 + 17.0
|
||||||
|
|
||||||
|
self.backgroundLayer.backgroundColor = theme.rootController.navigationSearchBar.inputFillColor.cgColor
|
||||||
|
self.backgroundLayer.cornerRadius = 10.5
|
||||||
|
|
||||||
|
let cancelTextSize = self.cancelButtonTitle.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(Text(
|
||||||
|
text: strings.Common_Cancel,
|
||||||
|
font: Font.regular(17.0),
|
||||||
|
color: theme.rootController.navigationBar.accentTextColor
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let cancelButtonSpacing: CGFloat = 8.0
|
||||||
|
|
||||||
|
var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight))
|
||||||
|
if isActiveWithText {
|
||||||
|
backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
|
||||||
|
|
||||||
|
transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height)))
|
||||||
|
|
||||||
|
let textX: CGFloat = backgroundFrame.minX + sideTextInset
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height))
|
||||||
|
self.textFrame = textFrame
|
||||||
|
|
||||||
|
if let image = self.iconView.image {
|
||||||
|
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
|
||||||
|
transition.setFrame(view: self.iconView, frame: iconFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
let placeholderSize = self.placeholderContent.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(
|
||||||
|
Text(text: strings.Common_Search, font: Font.regular(17.0), color: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
containerSize: size
|
||||||
|
)
|
||||||
|
if let placeholderContentView = self.placeholderContent.view {
|
||||||
|
if placeholderContentView.superview == nil {
|
||||||
|
self.addSubview(placeholderContentView)
|
||||||
|
}
|
||||||
|
let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.midY - placeholderSize.height / 2.0), size: placeholderSize)
|
||||||
|
transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = self.clearIconView.image {
|
||||||
|
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
|
||||||
|
transition.setFrame(view: self.clearIconView, frame: iconFrame)
|
||||||
|
transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let cancelButtonTitleComponentView = self.cancelButtonTitle.view {
|
||||||
|
if cancelButtonTitleComponentView.superview == nil {
|
||||||
|
self.addSubview(cancelButtonTitleComponentView)
|
||||||
|
cancelButtonTitleComponentView.isUserInteractionEnabled = false
|
||||||
|
}
|
||||||
|
transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let textField = self.textField {
|
||||||
|
textField.textColor = theme.rootController.navigationSearchBar.inputTextColor
|
||||||
|
transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideTextInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideTextInset - 32.0, height: backgroundFrame.height)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
@ -109,7 +109,10 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
Button(
|
Button(
|
||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Common_Close, font: Font.regular(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .left, maximumNumberOfLines: 1)
|
BundleIconComponent(
|
||||||
|
name: "Instant View/CloseIcon",
|
||||||
|
tintColor: environment.theme.rootController.navigationBar.accentTextColor
|
||||||
|
)
|
||||||
),
|
),
|
||||||
action: {
|
action: {
|
||||||
performAction.invoke(.close)
|
performAction.invoke(.close)
|
||||||
@ -130,7 +133,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||||||
content: LottieComponent.AppBundleContent(
|
content: LottieComponent.AppBundleContent(
|
||||||
name: "anim_moredots"
|
name: "anim_moredots"
|
||||||
),
|
),
|
||||||
color: environment.theme.rootController.navigationBar.primaryTextColor,
|
color: environment.theme.rootController.navigationBar.accentTextColor,
|
||||||
size: CGSize(width: 30.0, height: 30.0)
|
size: CGSize(width: 30.0, height: 30.0)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -150,7 +153,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||||||
ReferenceButtonComponent(
|
ReferenceButtonComponent(
|
||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
BundleIconComponent(
|
BundleIconComponent(
|
||||||
name: isLoading ? "Instant View/CloseIcon" : "Chat/Context Menu/Reload",
|
name: isLoading ? "Instant View/Close" : "Chat/Context Menu/Reload",
|
||||||
tintColor: environment.theme.rootController.navigationBar.primaryTextColor
|
tintColor: environment.theme.rootController.navigationBar.primaryTextColor
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -211,6 +214,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||||||
id: "navigation",
|
id: "navigation",
|
||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
NavigationToolbarContentComponent(
|
NavigationToolbarContentComponent(
|
||||||
|
accentColor: environment.theme.rootController.navigationBar.accentTextColor,
|
||||||
textColor: environment.theme.rootController.navigationBar.primaryTextColor,
|
textColor: environment.theme.rootController.navigationBar.primaryTextColor,
|
||||||
canGoBack: context.component.contentState?.canGoBack ?? false,
|
canGoBack: context.component.contentState?.canGoBack ?? false,
|
||||||
canGoForward: context.component.contentState?.canGoForward ?? false,
|
canGoForward: context.component.contentState?.canGoForward ?? false,
|
||||||
@ -281,6 +285,8 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||||||
case increaseFontSize
|
case increaseFontSize
|
||||||
case resetFontSize
|
case resetFontSize
|
||||||
case updateFontIsSerif(Bool)
|
case updateFontIsSerif(Bool)
|
||||||
|
case addBookmark
|
||||||
|
case openBookmarks
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate final class Node: ViewControllerTracingNode {
|
fileprivate final class Node: ViewControllerTracingNode {
|
||||||
@ -502,6 +508,12 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||||||
return updatedState
|
return updatedState
|
||||||
})
|
})
|
||||||
content.updateFontState(self.presentationState.fontState)
|
content.updateFontState(self.presentationState.fontState)
|
||||||
|
case .addBookmark:
|
||||||
|
if let content = self.content.last {
|
||||||
|
self.addBookmark(content.currentState.url)
|
||||||
|
}
|
||||||
|
case .openBookmarks:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -609,6 +621,43 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true))
|
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addBookmark(_ url: String) {
|
||||||
|
let _ = enqueueMessages(
|
||||||
|
account: self.context.account,
|
||||||
|
peerId: self.context.account.peerId,
|
||||||
|
messages: [.message(
|
||||||
|
text: url,
|
||||||
|
attributes: [],
|
||||||
|
inlineStickers: [:],
|
||||||
|
mediaReference: nil,
|
||||||
|
threadId: nil,
|
||||||
|
replyToMessageId: nil,
|
||||||
|
replyToStoryId: nil,
|
||||||
|
localGroupingKey: nil,
|
||||||
|
correlationId: nil,
|
||||||
|
bubbleUpEmojiOrStickersets: []
|
||||||
|
)]
|
||||||
|
).start()
|
||||||
|
|
||||||
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: presentationData.strings.WebBrowser_LinkAddedToBookmarks), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||||
|
if let self, action == .info {
|
||||||
|
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||||
|
guard let self, let peer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let navigationController = self.controller?.navigationController as? NavigationController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.minimize()
|
||||||
|
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}), in: .current)
|
||||||
|
}
|
||||||
|
|
||||||
private func setupContentStateUpdates() {
|
private func setupContentStateUpdates() {
|
||||||
for content in self.content {
|
for content in self.content {
|
||||||
content.onScrollingUpdate = { _ in }
|
content.onScrollingUpdate = { _ in }
|
||||||
@ -777,7 +826,11 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||||||
performAction.invoke(.updateSearchActive(true))
|
performAction.invoke(.updateSearchActive(true))
|
||||||
action(.default)
|
action(.default)
|
||||||
})),
|
})),
|
||||||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
|
.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||||
|
performAction.invoke(.addBookmark)
|
||||||
|
action(.default)
|
||||||
|
})),
|
||||||
|
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
|
||||||
if let self {
|
if let self {
|
||||||
self.context.sharedContext.applicationBindings.openUrl(openInUrl)
|
self.context.sharedContext.applicationBindings.openUrl(openInUrl)
|
||||||
}
|
}
|
||||||
@ -1074,7 +1127,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public var minimizedProgress: Float? {
|
public var minimizedProgress: Float? {
|
||||||
if let contentState = self.node.contentState {
|
if let contentState = self.node.contentState {
|
||||||
return Float(contentState.readingProgress)
|
return Float(contentState.readingProgress)
|
||||||
|
@ -33,7 +33,7 @@ final class SearchBarContentComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class View: UIView, UITextFieldDelegate {
|
final class View: UIView, UITextFieldDelegate {
|
||||||
private final class EmojiSearchTextField: UITextField {
|
private final class SearchTextField: UITextField {
|
||||||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||||
return bounds.integral
|
return bounds.integral
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ final class SearchBarContentComponent: Component {
|
|||||||
private var placeholderContent = ComponentView<Empty>()
|
private var placeholderContent = ComponentView<Empty>()
|
||||||
|
|
||||||
private var textFrame: CGRect?
|
private var textFrame: CGRect?
|
||||||
private var textField: EmojiSearchTextField?
|
private var textField: SearchTextField?
|
||||||
|
|
||||||
private var tapRecognizer: UITapGestureRecognizer?
|
private var tapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ final class SearchBarContentComponent: Component {
|
|||||||
let backgroundFrame = self.backgroundLayer.frame
|
let backgroundFrame = self.backgroundLayer.frame
|
||||||
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
|
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
|
||||||
|
|
||||||
let textField = EmojiSearchTextField(frame: textFieldFrame)
|
let textField = SearchTextField(frame: textFieldFrame)
|
||||||
textField.autocorrectionType = .no
|
textField.autocorrectionType = .no
|
||||||
textField.returnKeyType = .search
|
textField.returnKeyType = .search
|
||||||
self.textField = textField
|
self.textField = textField
|
||||||
@ -285,7 +285,7 @@ final class SearchBarContentComponent: Component {
|
|||||||
component: AnyComponent(Text(
|
component: AnyComponent(Text(
|
||||||
text: strings.Common_Cancel,
|
text: strings.Common_Cancel,
|
||||||
font: Font.regular(17.0),
|
font: Font.regular(17.0),
|
||||||
color: theme.rootController.navigationBar.primaryTextColor
|
color: theme.rootController.navigationBar.accentTextColor
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
||||||
|
@ -120,6 +120,7 @@ final class BrowserToolbarComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class NavigationToolbarContentComponent: CombinedComponent {
|
final class NavigationToolbarContentComponent: CombinedComponent {
|
||||||
|
let accentColor: UIColor
|
||||||
let textColor: UIColor
|
let textColor: UIColor
|
||||||
let canGoBack: Bool
|
let canGoBack: Bool
|
||||||
let canGoForward: Bool
|
let canGoForward: Bool
|
||||||
@ -127,12 +128,14 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||||||
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
accentColor: UIColor,
|
||||||
textColor: UIColor,
|
textColor: UIColor,
|
||||||
canGoBack: Bool,
|
canGoBack: Bool,
|
||||||
canGoForward: Bool,
|
canGoForward: Bool,
|
||||||
performAction: ActionSlot<BrowserScreen.Action>,
|
performAction: ActionSlot<BrowserScreen.Action>,
|
||||||
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
||||||
) {
|
) {
|
||||||
|
self.accentColor = accentColor
|
||||||
self.textColor = textColor
|
self.textColor = textColor
|
||||||
self.canGoBack = canGoBack
|
self.canGoBack = canGoBack
|
||||||
self.canGoForward = canGoForward
|
self.canGoForward = canGoForward
|
||||||
@ -141,6 +144,9 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool {
|
static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool {
|
||||||
|
if lhs.accentColor != rhs.accentColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.textColor != rhs.textColor {
|
if lhs.textColor != rhs.textColor {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -157,6 +163,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||||||
let back = Child(ContextReferenceButtonComponent.self)
|
let back = Child(ContextReferenceButtonComponent.self)
|
||||||
let forward = Child(ContextReferenceButtonComponent.self)
|
let forward = Child(ContextReferenceButtonComponent.self)
|
||||||
let share = Child(Button.self)
|
let share = Child(Button.self)
|
||||||
|
let bookmark = Child(Button.self)
|
||||||
let openIn = Child(Button.self)
|
let openIn = Child(Button.self)
|
||||||
|
|
||||||
return { context in
|
return { context in
|
||||||
@ -166,7 +173,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||||||
|
|
||||||
let sideInset: CGFloat = 5.0
|
let sideInset: CGFloat = 5.0
|
||||||
let buttonSize = CGSize(width: 50.0, height: availableSize.height)
|
let buttonSize = CGSize(width: 50.0, height: availableSize.height)
|
||||||
let spacing = (availableSize.width - buttonSize.width * 4.0 - sideInset * 2.0) / 3.0
|
let spacing = (availableSize.width - buttonSize.width * 5.0 - sideInset * 2.0) / 4.0
|
||||||
|
|
||||||
let canGoBack = context.component.canGoBack
|
let canGoBack = context.component.canGoBack
|
||||||
let back = back.update(
|
let back = back.update(
|
||||||
@ -174,7 +181,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
BundleIconComponent(
|
BundleIconComponent(
|
||||||
name: "Instant View/Back",
|
name: "Instant View/Back",
|
||||||
tintColor: canGoBack ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4)
|
tintColor: canGoBack ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
minSize: buttonSize,
|
minSize: buttonSize,
|
||||||
@ -202,7 +209,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
BundleIconComponent(
|
BundleIconComponent(
|
||||||
name: "Instant View/Forward",
|
name: "Instant View/Forward",
|
||||||
tintColor: canGoForward ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4)
|
tintColor: canGoForward ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
minSize: buttonSize,
|
minSize: buttonSize,
|
||||||
@ -229,7 +236,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
BundleIconComponent(
|
BundleIconComponent(
|
||||||
name: "Chat List/NavigationShare",
|
name: "Chat List/NavigationShare",
|
||||||
tintColor: context.component.textColor
|
tintColor: context.component.accentColor
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
action: {
|
action: {
|
||||||
@ -243,23 +250,42 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||||||
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width / 2.0, y: availableSize.height / 2.0))
|
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width / 2.0, y: availableSize.height / 2.0))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let bookmark = bookmark.update(
|
||||||
|
component: Button(
|
||||||
|
content: AnyComponent(
|
||||||
|
BundleIconComponent(
|
||||||
|
name: "Instant View/Bookmark",
|
||||||
|
tintColor: context.component.accentColor
|
||||||
|
)
|
||||||
|
),
|
||||||
|
action: {
|
||||||
|
performAction.invoke(.openBookmarks)
|
||||||
|
}
|
||||||
|
).minSize(buttonSize),
|
||||||
|
availableSize: buttonSize,
|
||||||
|
transition: .easeInOut(duration: 0.2)
|
||||||
|
)
|
||||||
|
context.add(bookmark
|
||||||
|
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width / 2.0, y: availableSize.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
let openIn = openIn.update(
|
let openIn = openIn.update(
|
||||||
component: Button(
|
component: Button(
|
||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
BundleIconComponent(
|
BundleIconComponent(
|
||||||
name: "Instant View/Minimize",
|
name: "Instant View/Browser",
|
||||||
tintColor: context.component.textColor
|
tintColor: context.component.accentColor
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
action: {
|
action: {
|
||||||
performAction.invoke(.minimize)
|
performAction.invoke(.openIn)
|
||||||
}
|
}
|
||||||
).minSize(buttonSize),
|
).minSize(buttonSize),
|
||||||
availableSize: buttonSize,
|
availableSize: buttonSize,
|
||||||
transition: .easeInOut(duration: 0.2)
|
transition: .easeInOut(duration: 0.2)
|
||||||
)
|
)
|
||||||
context.add(openIn
|
context.add(openIn
|
||||||
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0))
|
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0))
|
||||||
)
|
)
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
|
@ -204,9 +204,9 @@ private final class VideoRecorderImpl {
|
|||||||
|
|
||||||
if let videoInput = self.videoInput {
|
if let videoInput = self.videoInput {
|
||||||
let time = CACurrentMediaTime()
|
let time = CACurrentMediaTime()
|
||||||
if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime {
|
// if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime {
|
||||||
print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)")
|
// print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)")
|
||||||
}
|
// }
|
||||||
self.previousPresentationTime = presentationTime.seconds
|
self.previousPresentationTime = presentationTime.seconds
|
||||||
self.previousAppendTime = time
|
self.previousAppendTime = time
|
||||||
|
|
||||||
|
@ -80,7 +80,6 @@ public final class LocationViewController: ViewController {
|
|||||||
private let isStoryLocation: Bool
|
private let isStoryLocation: Bool
|
||||||
|
|
||||||
private let locationManager = LocationManager()
|
private let locationManager = LocationManager()
|
||||||
private var permissionDisposable: Disposable?
|
|
||||||
|
|
||||||
private var interaction: LocationViewInteraction?
|
private var interaction: LocationViewInteraction?
|
||||||
|
|
||||||
|
@ -100,9 +100,9 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||||||
case useLessVoiceData(PresentationTheme, String, Bool)
|
case useLessVoiceData(PresentationTheme, String, Bool)
|
||||||
case useLessVoiceDataInfo(PresentationTheme, String)
|
case useLessVoiceDataInfo(PresentationTheme, String)
|
||||||
case otherHeader(PresentationTheme, String)
|
case otherHeader(PresentationTheme, String)
|
||||||
|
case openLinksIn(PresentationTheme, String, String)
|
||||||
case shareSheet(PresentationTheme, String)
|
case shareSheet(PresentationTheme, String)
|
||||||
case saveEditedPhotos(PresentationTheme, String, Bool)
|
case saveEditedPhotos(PresentationTheme, String, Bool)
|
||||||
case openLinksIn(PresentationTheme, String, String)
|
|
||||||
case pauseMusicOnRecording(PresentationTheme, String, Bool)
|
case pauseMusicOnRecording(PresentationTheme, String, Bool)
|
||||||
case raiseToListen(PresentationTheme, String, Bool)
|
case raiseToListen(PresentationTheme, String, Bool)
|
||||||
case raiseToListenInfo(PresentationTheme, String)
|
case raiseToListenInfo(PresentationTheme, String)
|
||||||
@ -123,7 +123,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||||||
return DataAndStorageSection.backgroundDownload.rawValue
|
return DataAndStorageSection.backgroundDownload.rawValue
|
||||||
case .useLessVoiceData, .useLessVoiceDataInfo:
|
case .useLessVoiceData, .useLessVoiceDataInfo:
|
||||||
return DataAndStorageSection.voiceCalls.rawValue
|
return DataAndStorageSection.voiceCalls.rawValue
|
||||||
case .otherHeader, .shareSheet, .saveEditedPhotos, .openLinksIn, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo:
|
case .otherHeader, .openLinksIn, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo:
|
||||||
return DataAndStorageSection.other.rawValue
|
return DataAndStorageSection.other.rawValue
|
||||||
case .connectionHeader, .connectionProxy:
|
case .connectionHeader, .connectionProxy:
|
||||||
return DataAndStorageSection.connection.rawValue
|
return DataAndStorageSection.connection.rawValue
|
||||||
@ -162,11 +162,11 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||||||
return 24
|
return 24
|
||||||
case .otherHeader:
|
case .otherHeader:
|
||||||
return 29
|
return 29
|
||||||
case .shareSheet:
|
|
||||||
return 30
|
|
||||||
case .saveEditedPhotos:
|
|
||||||
return 31
|
|
||||||
case .openLinksIn:
|
case .openLinksIn:
|
||||||
|
return 30
|
||||||
|
case .shareSheet:
|
||||||
|
return 31
|
||||||
|
case .saveEditedPhotos:
|
||||||
return 32
|
return 32
|
||||||
case .pauseMusicOnRecording:
|
case .pauseMusicOnRecording:
|
||||||
return 33
|
return 33
|
||||||
@ -257,6 +257,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
case let .openLinksIn(lhsTheme, lhsText, lhsValue):
|
||||||
|
if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
case let .shareSheet(lhsTheme, lhsText):
|
case let .shareSheet(lhsTheme, lhsText):
|
||||||
if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
return true
|
return true
|
||||||
@ -269,12 +275,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .openLinksIn(lhsTheme, lhsText, lhsValue):
|
|
||||||
if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case let .pauseMusicOnRecording(lhsTheme, lhsText, lhsValue):
|
case let .pauseMusicOnRecording(lhsTheme, lhsText, lhsValue):
|
||||||
if case let .pauseMusicOnRecording(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
if case let .pauseMusicOnRecording(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||||
return true
|
return true
|
||||||
@ -386,6 +386,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||||
case let .otherHeader(_, text):
|
case let .otherHeader(_, text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
|
case let .openLinksIn(_, text, value):
|
||||||
|
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
|
||||||
|
arguments.openBrowserSelection()
|
||||||
|
})
|
||||||
case let .shareSheet(_, text):
|
case let .shareSheet(_, text):
|
||||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
|
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
|
||||||
arguments.openIntents()
|
arguments.openIntents()
|
||||||
@ -394,10 +398,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||||
arguments.toggleSaveEditedPhotos(value)
|
arguments.toggleSaveEditedPhotos(value)
|
||||||
}, tag: DataAndStorageEntryTag.saveEditedPhotos)
|
}, tag: DataAndStorageEntryTag.saveEditedPhotos)
|
||||||
case let .openLinksIn(_, text, value):
|
|
||||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
|
|
||||||
arguments.openBrowserSelection()
|
|
||||||
})
|
|
||||||
case let .pauseMusicOnRecording(_, text, value):
|
case let .pauseMusicOnRecording(_, text, value):
|
||||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||||
arguments.togglePauseMusicOnRecording(value)
|
arguments.togglePauseMusicOnRecording(value)
|
||||||
@ -618,11 +618,11 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
|
|||||||
entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription))
|
entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription))
|
||||||
|
|
||||||
entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other))
|
entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other))
|
||||||
|
entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser))
|
||||||
if #available(iOSApplicationExtension 13.2, iOS 13.2, *) {
|
if #available(iOSApplicationExtension 13.2, iOS 13.2, *) {
|
||||||
entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings))
|
entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings))
|
||||||
}
|
}
|
||||||
entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos))
|
entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos))
|
||||||
entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser))
|
|
||||||
entries.append(.pauseMusicOnRecording(presentationData.theme, presentationData.strings.Settings_PauseMusicOnRecording, data.mediaInputSettings.pauseMusicOnRecording))
|
entries.append(.pauseMusicOnRecording(presentationData.theme, presentationData.strings.Settings_PauseMusicOnRecording, data.mediaInputSettings.pauseMusicOnRecording))
|
||||||
entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak))
|
entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak))
|
||||||
entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo))
|
entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo))
|
||||||
|
@ -0,0 +1,448 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AccountContext
|
||||||
|
import UrlEscaping
|
||||||
|
|
||||||
|
private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||||
|
private var theme: PresentationTheme
|
||||||
|
private let backgroundNode: ASImageNode
|
||||||
|
fileprivate let textInputNode: EditableTextNode
|
||||||
|
private let placeholderNode: ASTextNode
|
||||||
|
|
||||||
|
var updateHeight: (() -> Void)?
|
||||||
|
var complete: (() -> Void)?
|
||||||
|
var textChanged: ((String) -> Void)?
|
||||||
|
|
||||||
|
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
|
||||||
|
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
|
||||||
|
|
||||||
|
var text: String {
|
||||||
|
get {
|
||||||
|
return self.textInputNode.attributedText?.string ?? ""
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor)
|
||||||
|
self.placeholderNode.isHidden = !newValue.isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholder: String = "" {
|
||||||
|
didSet {
|
||||||
|
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(theme: PresentationTheme, placeholder: String) {
|
||||||
|
self.theme = theme
|
||||||
|
|
||||||
|
self.backgroundNode = ASImageNode()
|
||||||
|
self.backgroundNode.isLayerBacked = true
|
||||||
|
self.backgroundNode.displaysAsynchronously = false
|
||||||
|
self.backgroundNode.displayWithoutProcessing = true
|
||||||
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||||
|
|
||||||
|
self.textInputNode = EditableTextNode()
|
||||||
|
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
|
||||||
|
self.textInputNode.clipsToBounds = true
|
||||||
|
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||||
|
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
|
||||||
|
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||||
|
self.textInputNode.keyboardType = .URL
|
||||||
|
self.textInputNode.autocapitalizationType = .none
|
||||||
|
self.textInputNode.returnKeyType = .done
|
||||||
|
self.textInputNode.autocorrectionType = .no
|
||||||
|
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
||||||
|
|
||||||
|
self.placeholderNode = ASTextNode()
|
||||||
|
self.placeholderNode.isUserInteractionEnabled = false
|
||||||
|
self.placeholderNode.displaysAsynchronously = false
|
||||||
|
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.textInputNode.delegate = self
|
||||||
|
|
||||||
|
self.addSubnode(self.backgroundNode)
|
||||||
|
self.addSubnode(self.textInputNode)
|
||||||
|
self.addSubnode(self.placeholderNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTheme(_ theme: PresentationTheme) {
|
||||||
|
self.theme = theme
|
||||||
|
|
||||||
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||||
|
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||||
|
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||||
|
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||||
|
let backgroundInsets = self.backgroundInsets
|
||||||
|
let inputInsets = self.inputInsets
|
||||||
|
|
||||||
|
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
|
||||||
|
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||||
|
|
||||||
|
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
|
||||||
|
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||||
|
|
||||||
|
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
|
||||||
|
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
|
||||||
|
|
||||||
|
return panelHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func activateInput() {
|
||||||
|
self.textInputNode.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deactivateInput() {
|
||||||
|
self.textInputNode.resignFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||||
|
self.updateTextNodeText(animated: true)
|
||||||
|
self.textChanged?(editableTextNode.textView.text)
|
||||||
|
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: [])
|
||||||
|
private let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: [])
|
||||||
|
|
||||||
|
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||||
|
if text == "\n" {
|
||||||
|
self.complete?()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let domainRegex = self.domainRegex, let pathRegex = self.pathRegex {
|
||||||
|
let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
|
||||||
|
let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
|
||||||
|
let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
|
||||||
|
|
||||||
|
if domainMatches.count > 0, pathMatches.count == 0 {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
|
||||||
|
let backgroundInsets = self.backgroundInsets
|
||||||
|
let inputInsets = self.inputInsets
|
||||||
|
|
||||||
|
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
|
||||||
|
|
||||||
|
return min(61.0, max(33.0, unboundTextFieldHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTextNodeText(animated: Bool) {
|
||||||
|
let backgroundInsets = self.backgroundInsets
|
||||||
|
|
||||||
|
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
|
||||||
|
|
||||||
|
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||||
|
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||||
|
self.updateHeight?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func clearPressed() {
|
||||||
|
self.textInputNode.attributedText = nil
|
||||||
|
self.deactivateInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class WebBrowserDomainAlertContentNode: AlertContentNode {
|
||||||
|
private let strings: PresentationStrings
|
||||||
|
|
||||||
|
private let titleNode: ASTextNode
|
||||||
|
private let textNode: ASTextNode
|
||||||
|
let inputFieldNode: WebBrowserDomainInputFieldNode
|
||||||
|
|
||||||
|
private let actionNodesSeparator: ASDisplayNode
|
||||||
|
private let actionNodes: [TextAlertContentActionNode]
|
||||||
|
private let actionVerticalSeparators: [ASDisplayNode]
|
||||||
|
|
||||||
|
private let disposable = MetaDisposable()
|
||||||
|
|
||||||
|
private var validLayout: CGSize?
|
||||||
|
|
||||||
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
|
var complete: (() -> Void)? {
|
||||||
|
didSet {
|
||||||
|
self.inputFieldNode.complete = self.complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var dismissOnOutsideTap: Bool {
|
||||||
|
return self.isUserInteractionEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) {
|
||||||
|
self.strings = strings
|
||||||
|
|
||||||
|
self.titleNode = ASTextNode()
|
||||||
|
self.titleNode.maximumNumberOfLines = 2
|
||||||
|
self.textNode = ASTextNode()
|
||||||
|
self.textNode.maximumNumberOfLines = 2
|
||||||
|
|
||||||
|
self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder)
|
||||||
|
self.inputFieldNode.text = ""
|
||||||
|
|
||||||
|
self.actionNodesSeparator = ASDisplayNode()
|
||||||
|
self.actionNodesSeparator.isLayerBacked = true
|
||||||
|
|
||||||
|
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||||
|
return TextAlertContentActionNode(theme: theme, action: action)
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||||
|
if actions.count > 1 {
|
||||||
|
for _ in 0 ..< actions.count - 1 {
|
||||||
|
let separatorNode = ASDisplayNode()
|
||||||
|
separatorNode.isLayerBacked = true
|
||||||
|
actionVerticalSeparators.append(separatorNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.actionVerticalSeparators = actionVerticalSeparators
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.titleNode)
|
||||||
|
self.addSubnode(self.textNode)
|
||||||
|
|
||||||
|
self.addSubnode(self.inputFieldNode)
|
||||||
|
|
||||||
|
self.addSubnode(self.actionNodesSeparator)
|
||||||
|
|
||||||
|
for actionNode in self.actionNodes {
|
||||||
|
self.addSubnode(actionNode)
|
||||||
|
}
|
||||||
|
self.actionNodes.last?.actionEnabled = false
|
||||||
|
|
||||||
|
for separatorNode in self.actionVerticalSeparators {
|
||||||
|
self.addSubnode(separatorNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inputFieldNode.updateHeight = { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if let _ = strongSelf.validLayout {
|
||||||
|
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inputFieldNode.textChanged = { [weak self] text in
|
||||||
|
if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
|
||||||
|
lastNode.actionEnabled = !text.isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateTheme(theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
var link: String {
|
||||||
|
return self.inputFieldNode.text
|
||||||
|
}
|
||||||
|
|
||||||
|
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||||
|
self.titleNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||||
|
|
||||||
|
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||||
|
for actionNode in self.actionNodes {
|
||||||
|
actionNode.updateTheme(theme)
|
||||||
|
}
|
||||||
|
for separatorNode in self.actionVerticalSeparators {
|
||||||
|
separatorNode.backgroundColor = theme.separatorColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if let size = self.validLayout {
|
||||||
|
_ = self.updateLayout(size: size, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||||
|
var size = size
|
||||||
|
size.width = min(size.width, 270.0)
|
||||||
|
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||||
|
|
||||||
|
let hadValidLayout = self.validLayout != nil
|
||||||
|
|
||||||
|
self.validLayout = size
|
||||||
|
|
||||||
|
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||||
|
let spacing: CGFloat = 5.0
|
||||||
|
|
||||||
|
let titleSize = self.titleNode.measure(measureSize)
|
||||||
|
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||||
|
origin.y += titleSize.height + 4.0
|
||||||
|
|
||||||
|
let textSize = self.textNode.measure(measureSize)
|
||||||
|
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||||
|
origin.y += textSize.height + 6.0 + spacing
|
||||||
|
|
||||||
|
let actionButtonHeight: CGFloat = 44.0
|
||||||
|
var minActionsWidth: CGFloat = 0.0
|
||||||
|
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||||
|
let actionTitleInsets: CGFloat = 8.0
|
||||||
|
|
||||||
|
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||||
|
for actionNode in self.actionNodes {
|
||||||
|
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||||
|
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||||
|
effectiveActionLayout = .vertical
|
||||||
|
}
|
||||||
|
switch effectiveActionLayout {
|
||||||
|
case .horizontal:
|
||||||
|
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||||
|
case .vertical:
|
||||||
|
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||||
|
|
||||||
|
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||||
|
contentWidth = max(contentWidth, 234.0)
|
||||||
|
|
||||||
|
var actionsHeight: CGFloat = 0.0
|
||||||
|
switch effectiveActionLayout {
|
||||||
|
case .horizontal:
|
||||||
|
actionsHeight = actionButtonHeight
|
||||||
|
case .vertical:
|
||||||
|
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultWidth = contentWidth + insets.left + insets.right
|
||||||
|
|
||||||
|
let inputFieldWidth = resultWidth
|
||||||
|
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||||
|
let inputHeight = inputFieldHeight
|
||||||
|
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
|
||||||
|
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
|
||||||
|
|
||||||
|
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||||
|
|
||||||
|
var actionOffset: CGFloat = 0.0
|
||||||
|
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||||
|
var separatorIndex = -1
|
||||||
|
var nodeIndex = 0
|
||||||
|
for actionNode in self.actionNodes {
|
||||||
|
if separatorIndex >= 0 {
|
||||||
|
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||||
|
switch effectiveActionLayout {
|
||||||
|
case .horizontal:
|
||||||
|
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||||
|
case .vertical:
|
||||||
|
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
separatorIndex += 1
|
||||||
|
|
||||||
|
let currentActionWidth: CGFloat
|
||||||
|
switch effectiveActionLayout {
|
||||||
|
case .horizontal:
|
||||||
|
if nodeIndex == self.actionNodes.count - 1 {
|
||||||
|
currentActionWidth = resultSize.width - actionOffset
|
||||||
|
} else {
|
||||||
|
currentActionWidth = actionWidth
|
||||||
|
}
|
||||||
|
case .vertical:
|
||||||
|
currentActionWidth = resultSize.width
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionNodeFrame: CGRect
|
||||||
|
switch effectiveActionLayout {
|
||||||
|
case .horizontal:
|
||||||
|
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||||
|
actionOffset += currentActionWidth
|
||||||
|
case .vertical:
|
||||||
|
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||||
|
actionOffset += actionButtonHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||||
|
|
||||||
|
nodeIndex += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hadValidLayout {
|
||||||
|
self.inputFieldNode.activateInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateError() {
|
||||||
|
self.inputFieldNode.layer.addShakeAnimation()
|
||||||
|
self.hapticFeedback.error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, apply: @escaping (String?) -> Void) -> AlertController {
|
||||||
|
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
var dismissImpl: ((Bool) -> Void)?
|
||||||
|
var applyImpl: (() -> Void)?
|
||||||
|
|
||||||
|
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||||
|
dismissImpl?(true)
|
||||||
|
apply(nil)
|
||||||
|
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
|
||||||
|
applyImpl?()
|
||||||
|
})]
|
||||||
|
|
||||||
|
let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions)
|
||||||
|
contentNode.complete = {
|
||||||
|
applyImpl?()
|
||||||
|
}
|
||||||
|
applyImpl = { [weak contentNode] in
|
||||||
|
guard let contentNode = contentNode else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let updatedLink = explicitUrl(contentNode.link)
|
||||||
|
if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) {
|
||||||
|
dismissImpl?(true)
|
||||||
|
apply(updatedLink)
|
||||||
|
} else {
|
||||||
|
contentNode.animateError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||||
|
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
|
||||||
|
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||||
|
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
|
||||||
|
})
|
||||||
|
controller.dismissed = { _ in
|
||||||
|
presentationDataDisposable.dispose()
|
||||||
|
}
|
||||||
|
dismissImpl = { [weak controller] animated in
|
||||||
|
contentNode.inputFieldNode.deactivateInput()
|
||||||
|
if animated {
|
||||||
|
controller?.dismissAnimated()
|
||||||
|
} else {
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return controller
|
||||||
|
}
|
@ -0,0 +1,280 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramPresentationData
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
import ItemListUI
|
||||||
|
|
||||||
|
public class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem {
|
||||||
|
let presentationData: ItemListPresentationData
|
||||||
|
let context: AccountContext?
|
||||||
|
let title: String
|
||||||
|
let label: String
|
||||||
|
public let sectionId: ItemListSectionId
|
||||||
|
let style: ItemListStyle
|
||||||
|
|
||||||
|
public init(
|
||||||
|
presentationData: ItemListPresentationData,
|
||||||
|
context: AccountContext? = nil,
|
||||||
|
title: String,
|
||||||
|
label: String,
|
||||||
|
sectionId: ItemListSectionId,
|
||||||
|
style: ItemListStyle
|
||||||
|
) {
|
||||||
|
self.presentationData = presentationData
|
||||||
|
self.context = context
|
||||||
|
self.title = title
|
||||||
|
self.label = label
|
||||||
|
self.sectionId = sectionId
|
||||||
|
self.style = style
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let node = WebBrowserDomainExceptionItemNode()
|
||||||
|
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||||
|
|
||||||
|
node.contentSize = layout.contentSize
|
||||||
|
node.insets = layout.insets
|
||||||
|
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
completion(node, {
|
||||||
|
return (nil, { _ in apply() })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if let nodeValue = node() as? WebBrowserDomainExceptionItemNode {
|
||||||
|
let makeLayout = nodeValue.asyncLayout()
|
||||||
|
|
||||||
|
async {
|
||||||
|
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
completion(layout, { _ in
|
||||||
|
apply()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var selectable: Bool = false
|
||||||
|
|
||||||
|
public func selected(listView: ListView){
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNode {
|
||||||
|
private let backgroundNode: ASDisplayNode
|
||||||
|
private let topStripeNode: ASDisplayNode
|
||||||
|
private let bottomStripeNode: ASDisplayNode
|
||||||
|
private let maskNode: ASImageNode
|
||||||
|
|
||||||
|
let iconNode: ASImageNode
|
||||||
|
let titleNode: TextNode
|
||||||
|
let labelNode: TextNode
|
||||||
|
|
||||||
|
private let activateArea: AccessibilityAreaNode
|
||||||
|
|
||||||
|
private var item: WebBrowserDomainExceptionItem?
|
||||||
|
|
||||||
|
override public var canBeSelected: Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public var tag: ItemListItemTag? = nil
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
self.backgroundNode = ASDisplayNode()
|
||||||
|
self.backgroundNode.isLayerBacked = true
|
||||||
|
self.backgroundNode.backgroundColor = .white
|
||||||
|
|
||||||
|
self.maskNode = ASImageNode()
|
||||||
|
self.maskNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.topStripeNode = ASDisplayNode()
|
||||||
|
self.topStripeNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.bottomStripeNode = ASDisplayNode()
|
||||||
|
self.bottomStripeNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.iconNode = ASImageNode()
|
||||||
|
self.iconNode.isLayerBacked = true
|
||||||
|
self.iconNode.displaysAsynchronously = false
|
||||||
|
|
||||||
|
self.titleNode = TextNode()
|
||||||
|
self.titleNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.labelNode = TextNode()
|
||||||
|
self.labelNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.activateArea = AccessibilityAreaNode()
|
||||||
|
|
||||||
|
super.init(layerBacked: false, dynamicBounce: false)
|
||||||
|
|
||||||
|
self.addSubnode(self.titleNode)
|
||||||
|
self.addSubnode(self.labelNode)
|
||||||
|
|
||||||
|
self.addSubnode(self.activateArea)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||||
|
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||||
|
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||||
|
|
||||||
|
let currentItem = self.item
|
||||||
|
|
||||||
|
return { item, params, neighbors in
|
||||||
|
var updatedTheme: PresentationTheme?
|
||||||
|
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||||
|
updatedTheme = item.presentationData.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentSize: CGSize
|
||||||
|
let insets: UIEdgeInsets
|
||||||
|
let separatorHeight = UIScreenPixel
|
||||||
|
let itemBackgroundColor: UIColor
|
||||||
|
let itemSeparatorColor: UIColor
|
||||||
|
|
||||||
|
let leftInset = 16.0 + params.leftInset + 43.0
|
||||||
|
|
||||||
|
let titleColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor
|
||||||
|
let labelColor: UIColor = item.presentationData.theme.list.itemAccentColor
|
||||||
|
|
||||||
|
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
|
||||||
|
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
|
||||||
|
|
||||||
|
let maxTitleWidth: CGFloat = params.width - params.rightInset - 20.0 - leftInset
|
||||||
|
|
||||||
|
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
let verticalInset: CGFloat = 11.0
|
||||||
|
let titleSpacing: CGFloat = 1.0
|
||||||
|
|
||||||
|
let height: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
|
||||||
|
|
||||||
|
switch item.style {
|
||||||
|
case .plain:
|
||||||
|
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||||
|
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||||
|
contentSize = CGSize(width: params.width, height: height)
|
||||||
|
insets = itemListNeighborsPlainInsets(neighbors)
|
||||||
|
case .blocks:
|
||||||
|
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||||
|
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||||
|
contentSize = CGSize(width: params.width, height: height)
|
||||||
|
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||||
|
|
||||||
|
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.item = item
|
||||||
|
|
||||||
|
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||||
|
strongSelf.activateArea.accessibilityLabel = item.title
|
||||||
|
strongSelf.activateArea.accessibilityValue = item.label
|
||||||
|
|
||||||
|
if let _ = updatedTheme {
|
||||||
|
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||||
|
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||||
|
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = titleApply()
|
||||||
|
let _ = labelApply()
|
||||||
|
|
||||||
|
switch item.style {
|
||||||
|
case .plain:
|
||||||
|
if strongSelf.backgroundNode.supernode != nil {
|
||||||
|
strongSelf.backgroundNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
if strongSelf.topStripeNode.supernode != nil {
|
||||||
|
strongSelf.topStripeNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
if strongSelf.bottomStripeNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||||
|
}
|
||||||
|
if strongSelf.maskNode.supernode != nil {
|
||||||
|
strongSelf.maskNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||||
|
case .blocks:
|
||||||
|
if strongSelf.backgroundNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||||
|
}
|
||||||
|
if strongSelf.topStripeNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||||
|
}
|
||||||
|
if strongSelf.bottomStripeNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||||
|
}
|
||||||
|
if strongSelf.maskNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||||
|
var hasTopCorners = false
|
||||||
|
var hasBottomCorners = false
|
||||||
|
switch neighbors.top {
|
||||||
|
case .sameSection(false):
|
||||||
|
strongSelf.topStripeNode.isHidden = true
|
||||||
|
default:
|
||||||
|
hasTopCorners = true
|
||||||
|
strongSelf.topStripeNode.isHidden = hasCorners
|
||||||
|
}
|
||||||
|
let bottomStripeInset: CGFloat
|
||||||
|
switch neighbors.bottom {
|
||||||
|
case .sameSection(false):
|
||||||
|
bottomStripeInset = leftInset
|
||||||
|
strongSelf.bottomStripeNode.isHidden = false
|
||||||
|
default:
|
||||||
|
bottomStripeInset = 0.0
|
||||||
|
hasBottomCorners = true
|
||||||
|
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||||
|
|
||||||
|
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||||
|
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||||
|
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||||
|
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
var centralContentHeight: CGFloat = titleLayout.size.height
|
||||||
|
centralContentHeight += titleSpacing
|
||||||
|
centralContentHeight += labelLayout.size.height
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size)
|
||||||
|
strongSelf.titleNode.frame = titleFrame
|
||||||
|
|
||||||
|
let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
|
||||||
|
strongSelf.labelNode.frame = labelFrame
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||||
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||||
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
}
|
@ -9,29 +9,69 @@ import TelegramUIPreferences
|
|||||||
import ItemListUI
|
import ItemListUI
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import OpenInExternalAppUI
|
import OpenInExternalAppUI
|
||||||
|
import ItemListPeerActionItem
|
||||||
|
import UndoUI
|
||||||
|
import WebKit
|
||||||
|
import LinkPresentation
|
||||||
|
|
||||||
private final class WebBrowserSettingsControllerArguments {
|
private final class WebBrowserSettingsControllerArguments {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let updateDefaultBrowser: (String?) -> Void
|
let updateDefaultBrowser: (String?) -> Void
|
||||||
|
let updateAutologin: (Bool) -> Void
|
||||||
|
let clearCookies: () -> Void
|
||||||
|
let addException: () -> Void
|
||||||
|
let clearExceptions: () -> Void
|
||||||
|
|
||||||
init(context: AccountContext, updateDefaultBrowser: @escaping (String?) -> Void) {
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
updateDefaultBrowser: @escaping (String?) -> Void,
|
||||||
|
updateAutologin: @escaping (Bool) -> Void,
|
||||||
|
clearCookies: @escaping () -> Void,
|
||||||
|
addException: @escaping () -> Void,
|
||||||
|
clearExceptions: @escaping () -> Void
|
||||||
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.updateDefaultBrowser = updateDefaultBrowser
|
self.updateDefaultBrowser = updateDefaultBrowser
|
||||||
|
self.updateAutologin = updateAutologin
|
||||||
|
self.clearCookies = clearCookies
|
||||||
|
self.addException = addException
|
||||||
|
self.clearExceptions = clearExceptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum WebBrowserSettingsSection: Int32 {
|
private enum WebBrowserSettingsSection: Int32 {
|
||||||
case browsers
|
case browsers
|
||||||
|
case autologin
|
||||||
|
case clearCookies
|
||||||
|
case exceptions
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
||||||
case browserHeader(PresentationTheme, String)
|
case browserHeader(PresentationTheme, String)
|
||||||
case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32)
|
case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32)
|
||||||
|
|
||||||
|
case autologin(PresentationTheme, String, Bool)
|
||||||
|
case autologinInfo(PresentationTheme, String)
|
||||||
|
|
||||||
|
case clearCookies(PresentationTheme, String)
|
||||||
|
case clearCookiesInfo(PresentationTheme, String)
|
||||||
|
|
||||||
|
case exceptionsHeader(PresentationTheme, String)
|
||||||
|
case exceptionsAdd(PresentationTheme, String)
|
||||||
|
case exception(Int32, PresentationTheme, WebBrowserException)
|
||||||
|
case exceptionsClear(PresentationTheme, String)
|
||||||
|
case exceptionsInfo(PresentationTheme, String)
|
||||||
|
|
||||||
var section: ItemListSectionId {
|
var section: ItemListSectionId {
|
||||||
switch self {
|
switch self {
|
||||||
case .browserHeader, .browser:
|
case .browserHeader, .browser:
|
||||||
return WebBrowserSettingsSection.browsers.rawValue
|
return WebBrowserSettingsSection.browsers.rawValue
|
||||||
|
case .autologin, .autologinInfo:
|
||||||
|
return WebBrowserSettingsSection.autologin.rawValue
|
||||||
|
case .clearCookies, .clearCookiesInfo:
|
||||||
|
return WebBrowserSettingsSection.clearCookies.rawValue
|
||||||
|
case .exceptionsHeader, .exceptionsAdd, .exception, .exceptionsClear, .exceptionsInfo:
|
||||||
|
return WebBrowserSettingsSection.exceptions.rawValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +81,24 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
|||||||
return 0
|
return 0
|
||||||
case let .browser(_, _, _, _, _, index):
|
case let .browser(_, _, _, _, _, index):
|
||||||
return 1 + index
|
return 1 + index
|
||||||
|
case .autologin:
|
||||||
|
return 100
|
||||||
|
case .autologinInfo:
|
||||||
|
return 101
|
||||||
|
case .clearCookies:
|
||||||
|
return 102
|
||||||
|
case .clearCookiesInfo:
|
||||||
|
return 103
|
||||||
|
case .exceptionsHeader:
|
||||||
|
return 104
|
||||||
|
case .exceptionsAdd:
|
||||||
|
return 105
|
||||||
|
case let .exception(index, _, _):
|
||||||
|
return 106 + index
|
||||||
|
case .exceptionsClear:
|
||||||
|
return 1000
|
||||||
|
case .exceptionsInfo:
|
||||||
|
return 1001
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +116,60 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
case let .autologin(lhsTheme, lhsText, lhsValue):
|
||||||
|
if case let .autologin(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .autologinInfo(lhsTheme, lhsText):
|
||||||
|
if case let .autologinInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .clearCookies(lhsTheme, lhsText):
|
||||||
|
if case let .clearCookies(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .clearCookiesInfo(lhsTheme, lhsText):
|
||||||
|
if case let .clearCookiesInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .exceptionsHeader(lhsTheme, lhsText):
|
||||||
|
if case let .exceptionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .exception(lhsIndex, lhsTheme, lhsException):
|
||||||
|
if case let .exception(rhsIndex, rhsTheme, rhsException) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsException == rhsException {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .exceptionsAdd(lhsTheme, lhsText):
|
||||||
|
if case let .exceptionsAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .exceptionsClear(lhsTheme, lhsText):
|
||||||
|
if case let .exceptionsClear(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .exceptionsInfo(lhsTheme, lhsText):
|
||||||
|
if case let .exceptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,44 +186,208 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
|||||||
return WebBrowserItem(context: arguments.context, presentationData: presentationData, title: title, application: application, checked: selected, sectionId: self.section) {
|
return WebBrowserItem(context: arguments.context, presentationData: presentationData, title: title, application: application, checked: selected, sectionId: self.section) {
|
||||||
arguments.updateDefaultBrowser(identifier)
|
arguments.updateDefaultBrowser(identifier)
|
||||||
}
|
}
|
||||||
|
case let .autologin(_, text, value):
|
||||||
|
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
|
||||||
|
arguments.updateAutologin(updatedValue)
|
||||||
|
})
|
||||||
|
case let .autologinInfo(_, text):
|
||||||
|
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||||
|
case let .clearCookies(_, text):
|
||||||
|
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
|
||||||
|
arguments.clearCookies()
|
||||||
|
})
|
||||||
|
case let .clearCookiesInfo(_, text):
|
||||||
|
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||||
|
case let .exceptionsHeader(_, text):
|
||||||
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
|
case let .exception(_, _, exception):
|
||||||
|
return WebBrowserDomainExceptionItem(presentationData: presentationData, context: arguments.context, title: exception.title, label: exception.domain, sectionId: self.section, style: .blocks)
|
||||||
|
case let .exceptionsAdd(_, text):
|
||||||
|
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
|
||||||
|
arguments.addException()
|
||||||
|
})
|
||||||
|
case let .exceptionsClear(_, text):
|
||||||
|
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
|
||||||
|
arguments.clearExceptions()
|
||||||
|
})
|
||||||
|
case let .exceptionsInfo(_, text):
|
||||||
|
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, selectedBrowser: String?) -> [WebBrowserSettingsControllerEntry] {
|
private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, settings: WebBrowserSettings) -> [WebBrowserSettingsControllerEntry] {
|
||||||
var entries: [WebBrowserSettingsControllerEntry] = []
|
var entries: [WebBrowserSettingsControllerEntry] = []
|
||||||
|
|
||||||
let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org"))
|
let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org"))
|
||||||
|
|
||||||
entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_DefaultBrowser))
|
entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title))
|
||||||
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, selectedBrowser == nil, 0))
|
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, settings.defaultWebBrowser == nil, 0))
|
||||||
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_InAppSafari, .safari, "inApp", selectedBrowser == "inApp", 1))
|
|
||||||
|
|
||||||
var index: Int32 = 2
|
var index: Int32 = 1
|
||||||
for option in options {
|
for option in options {
|
||||||
entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == selectedBrowser, index))
|
entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == settings.defaultWebBrowser, index))
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings.defaultWebBrowser == nil {
|
||||||
|
entries.append(.autologin(presentationData.theme, presentationData.strings.WebBrowser_AutoLogin, settings.autologin))
|
||||||
|
entries.append(.autologinInfo(presentationData.theme, presentationData.strings.WebBrowser_AutoLogin_Info))
|
||||||
|
|
||||||
|
entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies))
|
||||||
|
entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info))
|
||||||
|
|
||||||
|
entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Title))
|
||||||
|
entries.append(.exceptionsAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException))
|
||||||
|
|
||||||
|
var exceptionIndex: Int32 = 0
|
||||||
|
for exception in settings.exceptions {
|
||||||
|
entries.append(.exception(exceptionIndex, presentationData.theme, exception))
|
||||||
|
exceptionIndex += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.exceptions.isEmpty {
|
||||||
|
entries.append(.exceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Clear))
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.append(.exceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Info))
|
||||||
|
}
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
public func webBrowserSettingsController(context: AccountContext) -> ViewController {
|
public func webBrowserSettingsController(context: AccountContext) -> ViewController {
|
||||||
let arguments = WebBrowserSettingsControllerArguments(context: context, updateDefaultBrowser: { identifier in
|
var clearCookiesImpl: (() -> Void)?
|
||||||
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { $0.withUpdatedDefaultWebBrowser(identifier) }).start()
|
var addExceptionImpl: (() -> Void)?
|
||||||
})
|
var clearExceptionsImpl: (() -> Void)?
|
||||||
|
|
||||||
let signal = combineLatest(context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]))
|
let arguments = WebBrowserSettingsControllerArguments(
|
||||||
|
context: context,
|
||||||
|
updateDefaultBrowser: { identifier in
|
||||||
|
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, {
|
||||||
|
$0.withUpdatedDefaultWebBrowser(identifier)
|
||||||
|
}).start()
|
||||||
|
},
|
||||||
|
updateAutologin: { autologin in
|
||||||
|
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, {
|
||||||
|
$0.withUpdatedAutologin(autologin)
|
||||||
|
}).start()
|
||||||
|
},
|
||||||
|
clearCookies: {
|
||||||
|
clearCookiesImpl?()
|
||||||
|
},
|
||||||
|
addException: {
|
||||||
|
addExceptionImpl?()
|
||||||
|
},
|
||||||
|
clearExceptions: {
|
||||||
|
clearExceptionsImpl?()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let previousSettings = Atomic<WebBrowserSettings?>(value: nil)
|
||||||
|
|
||||||
|
let signal = combineLatest(
|
||||||
|
context.sharedContext.presentationData,
|
||||||
|
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings])
|
||||||
|
)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||||
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings
|
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings
|
||||||
|
let previousSettings = previousSettings.swap(settings)
|
||||||
|
|
||||||
|
var animateChanges = false
|
||||||
|
if let previousSettings {
|
||||||
|
if previousSettings.defaultWebBrowser != settings.defaultWebBrowser {
|
||||||
|
animateChanges = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
||||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, selectedBrowser: settings.defaultWebBrowser), style: .blocks, animateChanges: false)
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, settings: settings), style: .blocks, animateChanges: animateChanges)
|
||||||
|
|
||||||
return (controllerState, (listState, arguments))
|
return (controllerState, (listState, arguments))
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = ItemListController(context: context, state: signal)
|
let controller = ItemListController(context: context, state: signal)
|
||||||
|
|
||||||
|
clearCookiesImpl = { [weak controller] in
|
||||||
|
WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{})
|
||||||
|
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
controller?.present(UndoOverlayController(
|
||||||
|
presentationData: presentationData,
|
||||||
|
content: .info(
|
||||||
|
title: nil,
|
||||||
|
text: presentationData.strings.WebBrowser_ClearCookies_Succeed,
|
||||||
|
timeout: nil,
|
||||||
|
customUndoText: nil
|
||||||
|
),
|
||||||
|
elevatedLayout: false,
|
||||||
|
position: .bottom,
|
||||||
|
action: { _ in return false }), in: .current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addExceptionImpl = { [weak controller] in
|
||||||
|
let linkController = webBrowserDomainController(context: context, apply: { url in
|
||||||
|
if let url {
|
||||||
|
let _ = fetchDomainExceptionInfo(url: url).startStandalone(next: { newException in
|
||||||
|
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
|
||||||
|
var currentExceptions = currentSettings.exceptions
|
||||||
|
for exception in currentExceptions {
|
||||||
|
if exception.domain == newException.domain {
|
||||||
|
return currentSettings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentExceptions.append(newException)
|
||||||
|
return currentSettings.withUpdatedExceptions(currentExceptions)
|
||||||
|
}).start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
controller?.present(linkController, in: .window(.root))
|
||||||
|
}
|
||||||
|
|
||||||
|
clearExceptionsImpl = {
|
||||||
|
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
|
||||||
|
return currentSettings.withUpdatedExceptions([])
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func cleanDomain(url: String) -> (domain: String, fullUrl: String) {
|
||||||
|
if let parsedUrl = URL(string: url) {
|
||||||
|
let host: String?
|
||||||
|
let scheme = parsedUrl.scheme ?? "https"
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
host = parsedUrl.host(percentEncoded: true)?.lowercased()
|
||||||
|
} else {
|
||||||
|
host = parsedUrl.host?.lowercased()
|
||||||
|
}
|
||||||
|
return (host ?? url, "\(scheme)://\(host ?? "")")
|
||||||
|
} else {
|
||||||
|
return (url, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchDomainExceptionInfo(url: String) -> Signal<WebBrowserException, NoError> {
|
||||||
|
let (domain, domainUrl) = cleanDomain(url: url)
|
||||||
|
if #available(iOS 13.0, *), let url = URL(string: domainUrl) {
|
||||||
|
return Signal { subscriber in
|
||||||
|
let metadataProvider = LPMetadataProvider()
|
||||||
|
metadataProvider.shouldFetchSubresources = true
|
||||||
|
metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in
|
||||||
|
let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title
|
||||||
|
subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain))
|
||||||
|
subscriber.putCompletion()
|
||||||
|
})
|
||||||
|
return ActionDisposable {
|
||||||
|
metadataProvider.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .single(WebBrowserException(domain: domain, title: domain))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -45,6 +45,7 @@ public enum PresentationResourceKey: Int32 {
|
|||||||
case itemListSecondaryCheckIcon
|
case itemListSecondaryCheckIcon
|
||||||
case itemListPlusIcon
|
case itemListPlusIcon
|
||||||
case itemListRoundPlusIcon
|
case itemListRoundPlusIcon
|
||||||
|
case itemListAccentDeleteIcon
|
||||||
case itemListDeleteIcon
|
case itemListDeleteIcon
|
||||||
case itemListDeleteIndicatorIcon
|
case itemListDeleteIndicatorIcon
|
||||||
case itemListReorderIndicatorIcon
|
case itemListReorderIndicatorIcon
|
||||||
|
@ -69,6 +69,12 @@ public struct PresentationResourcesItemList {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func accentDeleteIconImage(_ theme: PresentationTheme) -> UIImage? {
|
||||||
|
return theme.image(PresentationResourceKey.itemListAccentDeleteIcon.rawValue, { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemAccentColor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? {
|
public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? {
|
||||||
return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in
|
return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor)
|
||||||
|
@ -338,6 +338,19 @@ private final class MediaCoverScreenComponent: Component {
|
|||||||
mediaEditor.seek(position, andPlay: false)
|
mediaEditor.seek(position, andPlay: false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
coverPositionUpdated: { [weak mediaEditor] position, tap, commit in
|
||||||
|
if let mediaEditor {
|
||||||
|
if tap {
|
||||||
|
mediaEditor.setOnNextDisplay {
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
mediaEditor.seek(position, andPlay: false)
|
||||||
|
} else {
|
||||||
|
mediaEditor.seek(position, andPlay: false)
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
trackTrimUpdated: { _, _, _, _, _ in
|
trackTrimUpdated: { _, _, _, _, _ in
|
||||||
},
|
},
|
||||||
trackOffsetUpdated: { _, _, _ in
|
trackOffsetUpdated: { _, _, _ in
|
||||||
|
@ -48,6 +48,7 @@ import StickerPackEditTitleController
|
|||||||
import StickerPickerScreen
|
import StickerPickerScreen
|
||||||
import UIKitRuntimeUtils
|
import UIKitRuntimeUtils
|
||||||
import ImageObjectSeparation
|
import ImageObjectSeparation
|
||||||
|
import DeviceAccess
|
||||||
|
|
||||||
private let playbackButtonTag = GenericComponentViewTag()
|
private let playbackButtonTag = GenericComponentViewTag()
|
||||||
private let muteButtonTag = GenericComponentViewTag()
|
private let muteButtonTag = GenericComponentViewTag()
|
||||||
@ -2553,6 +2554,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
|
|
||||||
var recording: MediaEditorScreen.Recording
|
var recording: MediaEditorScreen.Recording
|
||||||
|
|
||||||
|
private let locationManager = LocationManager()
|
||||||
|
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private var validLayout: ContainerViewLayout?
|
private var validLayout: ContainerViewLayout?
|
||||||
|
|
||||||
@ -4584,7 +4587,37 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.mediaEditor?.play()
|
self.mediaEditor?.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather) {
|
func requestWeather() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentLocationAccessAlert() {
|
||||||
|
DeviceAccess.authorizeAccess(to: .location(.send), locationManager: self.locationManager, presentationData: self.presentationData, present: { [weak self] c, a in
|
||||||
|
self?.controller?.present(c, in: .window(.root), with: a)
|
||||||
|
}, openSettings: { [weak self] in
|
||||||
|
self?.context.sharedContext.applicationBindings.openSettings()
|
||||||
|
}, { [weak self] authorized in
|
||||||
|
guard let self, authorized else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let weatherPromise = Promise<StickerPickerScreen.Weather>()
|
||||||
|
weatherPromise.set(getWeather(context: self.context))
|
||||||
|
self.weatherPromise = weatherPromise
|
||||||
|
|
||||||
|
let _ = (weatherPromise.get()
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||||
|
if let self, case let .loaded(weather) = result {
|
||||||
|
self.addWeather(weather)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) {
|
||||||
|
guard let weather else {
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
let maxWeatherCount = 3
|
let maxWeatherCount = 3
|
||||||
var currentWeatherCount = 0
|
var currentWeatherCount = 0
|
||||||
self.entitiesView.eachView { entityView in
|
self.entitiesView.eachView { entityView in
|
||||||
@ -4948,9 +4981,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
if let self {
|
if let self {
|
||||||
if let weatherPromise = self.weatherPromise {
|
if let weatherPromise = self.weatherPromise {
|
||||||
let _ = (weatherPromise.get()
|
let _ = (weatherPromise.get()
|
||||||
|> take(1)).start(next: { [weak self] weather in
|
|> take(1)).start(next: { [weak self] result in
|
||||||
if let self, case let .loaded(loaded) = weather {
|
if let self {
|
||||||
self.addWeather(loaded)
|
switch result {
|
||||||
|
case let .loaded(weather):
|
||||||
|
self.addWeather(weather)
|
||||||
|
case .notDetermined, .notAllowed:
|
||||||
|
self.presentLocationAccessAlert()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,7 @@ public final class MediaScrubberComponent: Component {
|
|||||||
let portalView: PortalView?
|
let portalView: PortalView?
|
||||||
|
|
||||||
let positionUpdated: (Double, Bool) -> Void
|
let positionUpdated: (Double, Bool) -> Void
|
||||||
|
let coverPositionUpdated: (Double, Bool, @escaping () -> Void) -> Void
|
||||||
let trackTrimUpdated: (Int32, Double, Double, Bool, Bool) -> Void
|
let trackTrimUpdated: (Int32, Double, Double, Bool, Bool) -> Void
|
||||||
let trackOffsetUpdated: (Int32, Double, Bool) -> Void
|
let trackOffsetUpdated: (Int32, Double, Bool) -> Void
|
||||||
let trackLongPressed: (Int32, UIView) -> Void
|
let trackLongPressed: (Int32, UIView) -> Void
|
||||||
@ -104,6 +105,7 @@ public final class MediaScrubberComponent: Component {
|
|||||||
tracks: [Track],
|
tracks: [Track],
|
||||||
portalView: PortalView? = nil,
|
portalView: PortalView? = nil,
|
||||||
positionUpdated: @escaping (Double, Bool) -> Void,
|
positionUpdated: @escaping (Double, Bool) -> Void,
|
||||||
|
coverPositionUpdated: @escaping (Double, Bool, @escaping () -> Void) -> Void = { _, _, _ in },
|
||||||
trackTrimUpdated: @escaping (Int32, Double, Double, Bool, Bool) -> Void,
|
trackTrimUpdated: @escaping (Int32, Double, Double, Bool, Bool) -> Void,
|
||||||
trackOffsetUpdated: @escaping (Int32, Double, Bool) -> Void,
|
trackOffsetUpdated: @escaping (Int32, Double, Bool) -> Void,
|
||||||
trackLongPressed: @escaping (Int32, UIView) -> Void
|
trackLongPressed: @escaping (Int32, UIView) -> Void
|
||||||
@ -119,6 +121,7 @@ public final class MediaScrubberComponent: Component {
|
|||||||
self.tracks = tracks
|
self.tracks = tracks
|
||||||
self.portalView = portalView
|
self.portalView = portalView
|
||||||
self.positionUpdated = positionUpdated
|
self.positionUpdated = positionUpdated
|
||||||
|
self.coverPositionUpdated = coverPositionUpdated
|
||||||
self.trackTrimUpdated = trackTrimUpdated
|
self.trackTrimUpdated = trackTrimUpdated
|
||||||
self.trackOffsetUpdated = trackOffsetUpdated
|
self.trackOffsetUpdated = trackOffsetUpdated
|
||||||
self.trackLongPressed = trackLongPressed
|
self.trackLongPressed = trackLongPressed
|
||||||
@ -164,6 +167,7 @@ public final class MediaScrubberComponent: Component {
|
|||||||
|
|
||||||
private var selectedTrackId: Int32 = 0
|
private var selectedTrackId: Int32 = 0
|
||||||
private var isPanningCursor = false
|
private var isPanningCursor = false
|
||||||
|
private var ignoreCursorPositionUpdate = false
|
||||||
|
|
||||||
private var scrubberSize: CGSize?
|
private var scrubberSize: CGSize?
|
||||||
|
|
||||||
@ -327,10 +331,18 @@ public final class MediaScrubberComponent: Component {
|
|||||||
switch gestureRecognizer.state {
|
switch gestureRecognizer.state {
|
||||||
case .began, .changed:
|
case .began, .changed:
|
||||||
self.isPanningCursor = true
|
self.isPanningCursor = true
|
||||||
component.positionUpdated(position, false)
|
if case .cover = component.style {
|
||||||
|
component.coverPositionUpdated(position, false, {})
|
||||||
|
} else {
|
||||||
|
component.positionUpdated(position, false)
|
||||||
|
}
|
||||||
case .ended, .cancelled:
|
case .ended, .cancelled:
|
||||||
self.isPanningCursor = false
|
self.isPanningCursor = false
|
||||||
component.positionUpdated(position, true)
|
if case .cover = component.style {
|
||||||
|
component.coverPositionUpdated(position, false, {})
|
||||||
|
} else {
|
||||||
|
component.positionUpdated(position, true)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -345,7 +357,7 @@ public final class MediaScrubberComponent: Component {
|
|||||||
var y: CGFloat = -5.0 - UIScreenPixel
|
var y: CGFloat = -5.0 - UIScreenPixel
|
||||||
if let component = self.component, case .cover = component.style {
|
if let component = self.component, case .cover = component.style {
|
||||||
cursorWidth = 30.0 + 12.0
|
cursorWidth = 30.0 + 12.0
|
||||||
cursorMargin = 0.0
|
cursorMargin = handleWidth
|
||||||
height = 50.0
|
height = 50.0
|
||||||
isCover = true
|
isCover = true
|
||||||
y += 1.0
|
y += 1.0
|
||||||
@ -472,6 +484,23 @@ public final class MediaScrubberComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
trackTransition = .immediate
|
trackTransition = .immediate
|
||||||
trackView = TrackView()
|
trackView = TrackView()
|
||||||
|
trackView.onTap = { [weak self] fraction in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var position = max(self.startPosition, min(self.endPosition, self.trimDuration * fraction))
|
||||||
|
if let offset = self.mainAudioTrackOffset {
|
||||||
|
position += offset
|
||||||
|
}
|
||||||
|
self.ignoreCursorPositionUpdate = true
|
||||||
|
component.coverPositionUpdated(position, true, { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.ignoreCursorPositionUpdate = false
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
})
|
||||||
|
}
|
||||||
trackView.onSelection = { [weak self] id in
|
trackView.onSelection = { [weak self] id in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -659,13 +688,15 @@ public final class MediaScrubberComponent: Component {
|
|||||||
self.cursorPositionAnimation = nil
|
self.cursorPositionAnimation = nil
|
||||||
self.cursorDisplayLink?.isPaused = true
|
self.cursorDisplayLink?.isPaused = true
|
||||||
|
|
||||||
var cursorPosition = component.position
|
if !self.ignoreCursorPositionUpdate {
|
||||||
if let offset = self.mainAudioTrackOffset {
|
var cursorPosition = component.position
|
||||||
cursorPosition -= offset
|
if let offset = self.mainAudioTrackOffset {
|
||||||
|
cursorPosition -= offset
|
||||||
|
}
|
||||||
|
let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration)
|
||||||
|
transition.setFrame(view: self.cursorView, frame: cursorFrame)
|
||||||
|
transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0))
|
||||||
}
|
}
|
||||||
let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration)
|
|
||||||
transition.setFrame(view: self.cursorView, frame: cursorFrame)
|
|
||||||
transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0))
|
|
||||||
} else {
|
} else {
|
||||||
if let (_, _, end, ended) = self.cursorPositionAnimation {
|
if let (_, _, end, ended) = self.cursorPositionAnimation {
|
||||||
if ended, component.position >= self.startPosition && component.position < end - 1.0 {
|
if ended, component.position >= self.startPosition && component.position < end - 1.0 {
|
||||||
@ -718,6 +749,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||||||
fileprivate var videoOpaqueFrameLayers: [VideoFrameLayer] = []
|
fileprivate var videoOpaqueFrameLayers: [VideoFrameLayer] = []
|
||||||
|
|
||||||
var onSelection: (Int32) -> Void = { _ in }
|
var onSelection: (Int32) -> Void = { _ in }
|
||||||
|
var onTap: (CGFloat) -> Void = { _ in }
|
||||||
var offsetUpdated: (Double, Bool) -> Void = { _, _ in }
|
var offsetUpdated: (Double, Bool) -> Void = { _, _ in }
|
||||||
var updated: (ComponentTransition) -> Void = { _ in }
|
var updated: (ComponentTransition) -> Void = { _ in }
|
||||||
|
|
||||||
@ -794,10 +826,15 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||||
guard let (track, _, _, _) = self.params else {
|
guard let params = self.params else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.onSelection(track.id)
|
if case .cover = params.style {
|
||||||
|
let location = gestureRecognizer.location(in: self)
|
||||||
|
self.onTap(location.x / self.frame.width)
|
||||||
|
} else {
|
||||||
|
self.onSelection(params.track.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTrackOffset(done: Bool) {
|
private func updateTrackOffset(done: Bool) {
|
||||||
@ -841,6 +878,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var params: (
|
private var params: (
|
||||||
|
style: MediaScrubberComponent.Style,
|
||||||
track: MediaScrubberComponent.Track,
|
track: MediaScrubberComponent.Track,
|
||||||
isSelected: Bool,
|
isSelected: Bool,
|
||||||
availableSize: CGSize,
|
availableSize: CGSize,
|
||||||
@ -889,7 +927,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||||||
transition: ComponentTransition
|
transition: ComponentTransition
|
||||||
) -> CGSize {
|
) -> CGSize {
|
||||||
let previousParams = self.params
|
let previousParams = self.params
|
||||||
self.params = (track, isSelected, availableSize, duration)
|
self.params = (style, track, isSelected, availableSize, duration)
|
||||||
|
|
||||||
let fullTrackHeight: CGFloat
|
let fullTrackHeight: CGFloat
|
||||||
let framesCornerRadius: CGFloat
|
let framesCornerRadius: CGFloat
|
||||||
|
@ -2014,7 +2014,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
hasCategories = true
|
hasCategories = true
|
||||||
}
|
}
|
||||||
let sendAsPeersCount = component.stateContext.stateValue?.sendAsPeers.count ?? 1
|
let sendAsPeersCount = component.stateContext.stateValue?.sendAsPeers.count ?? 1
|
||||||
if sendAsPeersCount > 1 && !"".isEmpty {
|
if sendAsPeersCount > 1 {
|
||||||
hasChannels = true
|
hasChannels = true
|
||||||
}
|
}
|
||||||
if let currentHasChannels = self.currentHasChannels, currentHasChannels != hasChannels {
|
if let currentHasChannels = self.currentHasChannels, currentHasChannels != hasChannels {
|
||||||
@ -2618,7 +2618,11 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
||||||
} else {
|
} else {
|
||||||
if !hasCategories {
|
if !hasCategories {
|
||||||
inset = 314.0
|
if self.selectedOptions.contains(.pin) {
|
||||||
|
inset = 422.0
|
||||||
|
} else {
|
||||||
|
inset = 314.0
|
||||||
|
}
|
||||||
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
||||||
} else {
|
} else {
|
||||||
if hasChannels {
|
if hasChannels {
|
||||||
|
@ -81,7 +81,11 @@ struct CameraState: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updatedRecording(_ recording: Recording) -> CameraState {
|
func updatedRecording(_ recording: Recording) -> CameraState {
|
||||||
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
|
var flashModeDidChange = self.flashModeDidChange
|
||||||
|
if case .none = self.recording {
|
||||||
|
flashModeDidChange = false
|
||||||
|
}
|
||||||
|
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatedDuration(_ duration: Double) -> CameraState {
|
func updatedDuration(_ duration: Double) -> CameraState {
|
||||||
@ -408,11 +412,11 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||||||
let isFirstRecording = initialDuration.isZero
|
let isFirstRecording = initialDuration.isZero
|
||||||
controller.node.resumeCameraCapture()
|
controller.node.resumeCameraCapture()
|
||||||
|
|
||||||
controller.updatePreviewState({ _ in return nil}, transition: .spring(duration: 0.4))
|
|
||||||
|
|
||||||
controller.node.dismissAllTooltips()
|
controller.node.dismissAllTooltips()
|
||||||
controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4))
|
controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4))
|
||||||
|
|
||||||
|
controller.updatePreviewState({ _ in return nil }, transition: .spring(duration: 0.4))
|
||||||
|
|
||||||
controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) {
|
controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) {
|
||||||
Queue.mainQueue().after(0.15) {
|
Queue.mainQueue().after(0.15) {
|
||||||
self.resultDisposable.set((camera.startRecording()
|
self.resultDisposable.set((camera.startRecording()
|
||||||
@ -438,6 +442,10 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||||||
if initialDuration > 0.0 {
|
if initialDuration > 0.0 {
|
||||||
controller.onResume()
|
controller.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if controller.cameraState.position == .front && controller.cameraState.flashMode == .on {
|
||||||
|
self.updateScreenBrightness()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopVideoRecording() {
|
func stopVideoRecording() {
|
||||||
@ -645,7 +653,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||||||
action: { [weak state] in
|
action: { [weak state] in
|
||||||
if let state {
|
if let state {
|
||||||
state.toggleFlashMode()
|
state.toggleFlashMode()
|
||||||
flashAction.invoke(Void())
|
Queue.mainQueue().justDispatch {
|
||||||
|
flashAction.invoke(Void())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf
vendored
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "ic_lt_safari.pdf",
|
"filename" : "Bookmark.pdf",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Browser.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
83
submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf
vendored
Normal file
83
submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 6.169983 6.370150 cm
|
||||||
|
1.000000 1.000000 1.000000 scn
|
||||||
|
0.359774 1.100077 m
|
||||||
|
0.100075 0.840378 0.100075 0.419323 0.359774 0.159624 c
|
||||||
|
0.619473 -0.100075 1.040527 -0.100075 1.300226 0.159624 c
|
||||||
|
8.830000 7.689398 l
|
||||||
|
16.359774 0.159624 l
|
||||||
|
16.619473 -0.100075 17.040527 -0.100075 17.300226 0.159624 c
|
||||||
|
17.559925 0.419323 17.559925 0.840378 17.300226 1.100077 c
|
||||||
|
9.770452 8.629850 l
|
||||||
|
17.300226 16.159624 l
|
||||||
|
17.559925 16.419323 17.559925 16.840378 17.300226 17.100077 c
|
||||||
|
17.040527 17.359776 16.619473 17.359776 16.359774 17.100077 c
|
||||||
|
8.830000 9.570303 l
|
||||||
|
1.300226 17.100077 l
|
||||||
|
1.040527 17.359776 0.619473 17.359776 0.359774 17.100077 c
|
||||||
|
0.100075 16.840378 0.100075 16.419323 0.359774 16.159624 c
|
||||||
|
7.889547 8.629850 l
|
||||||
|
0.359774 1.100077 l
|
||||||
|
h
|
||||||
|
f*
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
788
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||||
|
/Resources 1 0 R
|
||||||
|
/Contents 2 0 R
|
||||||
|
/Parent 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Kids [ 4 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Pages 5 0 R
|
||||||
|
/Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000000034 00000 n
|
||||||
|
0000000878 00000 n
|
||||||
|
0000000900 00000 n
|
||||||
|
0000001073 00000 n
|
||||||
|
0000001147 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
1206
|
||||||
|
%%EOF
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "cross.pdf",
|
"filename" : "Close.pdf",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
%PDF-1.7
|
|
||||||
|
|
||||||
1 0 obj
|
|
||||||
<< >>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
2 0 obj
|
|
||||||
<< /Length 3 0 R >>
|
|
||||||
stream
|
|
||||||
/DeviceRGB CS
|
|
||||||
/DeviceRGB cs
|
|
||||||
q
|
|
||||||
1.000000 0.000000 -0.000000 1.000000 5.100098 4.640198 cm
|
|
||||||
1.000000 1.000000 1.000000 scn
|
|
||||||
0.970226 14.230053 m
|
|
||||||
0.710527 14.489752 0.289473 14.489752 0.029774 14.230053 c
|
|
||||||
-0.229925 13.970354 -0.229925 13.549299 0.029774 13.289600 c
|
|
||||||
5.959549 7.359825 l
|
|
||||||
0.029774 1.430050 l
|
|
||||||
-0.229925 1.170351 -0.229925 0.749296 0.029774 0.489597 c
|
|
||||||
0.289473 0.229898 0.710527 0.229898 0.970226 0.489597 c
|
|
||||||
6.900002 6.419373 l
|
|
||||||
12.829774 0.489600 l
|
|
||||||
13.089473 0.229901 13.510528 0.229901 13.770226 0.489600 c
|
|
||||||
14.029925 0.749299 14.029925 1.170354 13.770226 1.430053 c
|
|
||||||
7.840454 7.359825 l
|
|
||||||
13.770226 13.289598 l
|
|
||||||
14.029925 13.549296 14.029925 13.970351 13.770226 14.230050 c
|
|
||||||
13.510528 14.489749 13.089473 14.489749 12.829774 14.230050 c
|
|
||||||
6.900002 8.300278 l
|
|
||||||
0.970226 14.230053 l
|
|
||||||
h
|
|
||||||
f*
|
|
||||||
n
|
|
||||||
Q
|
|
||||||
|
|
||||||
endstream
|
|
||||||
endobj
|
|
||||||
|
|
||||||
3 0 obj
|
|
||||||
789
|
|
||||||
endobj
|
|
||||||
|
|
||||||
4 0 obj
|
|
||||||
<< /Annots []
|
|
||||||
/Type /Page
|
|
||||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
|
||||||
/Resources 1 0 R
|
|
||||||
/Contents 2 0 R
|
|
||||||
/Parent 5 0 R
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
5 0 obj
|
|
||||||
<< /Kids [ 4 0 R ]
|
|
||||||
/Count 1
|
|
||||||
/Type /Pages
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
6 0 obj
|
|
||||||
<< /Pages 5 0 R
|
|
||||||
/Type /Catalog
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
xref
|
|
||||||
0 7
|
|
||||||
0000000000 65535 f
|
|
||||||
0000000010 00000 n
|
|
||||||
0000000034 00000 n
|
|
||||||
0000000879 00000 n
|
|
||||||
0000000901 00000 n
|
|
||||||
0000001074 00000 n
|
|
||||||
0000001148 00000 n
|
|
||||||
trailer
|
|
||||||
<< /ID [ (some) (id) ]
|
|
||||||
/Root 6 0 R
|
|
||||||
/Size 7
|
|
||||||
>>
|
|
||||||
startxref
|
|
||||||
1207
|
|
||||||
%%EOF
|
|
Binary file not shown.
@ -1033,7 +1033,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||||||
}
|
}
|
||||||
if accessChallengeData.data.isLockable {
|
if accessChallengeData.data.isLockable {
|
||||||
if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil {
|
if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil {
|
||||||
settings = WebBrowserSettings(defaultWebBrowser: "safari")
|
settings = WebBrowserSettings(defaultWebBrowser: "safari", autologin: false, exceptions: [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return settings
|
return settings
|
||||||
|
@ -3,35 +3,84 @@ import Postbox
|
|||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
|
||||||
public struct WebBrowserSettings: Codable, Equatable {
|
public struct WebBrowserException: Codable, Equatable {
|
||||||
public let defaultWebBrowser: String?
|
public let domain: String
|
||||||
|
public let title: String
|
||||||
|
|
||||||
public static var defaultSettings: WebBrowserSettings {
|
public init(domain: String, title: String) {
|
||||||
return WebBrowserSettings(defaultWebBrowser: nil)
|
self.domain = domain
|
||||||
|
self.title = title
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(defaultWebBrowser: String?) {
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||||
|
|
||||||
|
self.domain = try container.decode(String.self, forKey: "domain")
|
||||||
|
self.title = try container.decode(String.self, forKey: "title")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||||
|
|
||||||
|
try container.encode(self.domain, forKey: "domain")
|
||||||
|
try container.encode(self.title, forKey: "title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct WebBrowserSettings: Codable, Equatable {
|
||||||
|
public let defaultWebBrowser: String?
|
||||||
|
public let autologin: Bool
|
||||||
|
public let exceptions: [WebBrowserException]
|
||||||
|
|
||||||
|
public static var defaultSettings: WebBrowserSettings {
|
||||||
|
return WebBrowserSettings(defaultWebBrowser: nil, autologin: true, exceptions: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(defaultWebBrowser: String?, autologin: Bool, exceptions: [WebBrowserException]) {
|
||||||
self.defaultWebBrowser = defaultWebBrowser
|
self.defaultWebBrowser = defaultWebBrowser
|
||||||
|
self.autologin = autologin
|
||||||
|
self.exceptions = exceptions
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||||
|
|
||||||
self.defaultWebBrowser = try? container.decodeIfPresent(String.self, forKey: "defaultWebBrowser")
|
self.defaultWebBrowser = try? container.decodeIfPresent(String.self, forKey: "defaultWebBrowser")
|
||||||
|
self.autologin = (try? container.decodeIfPresent(Bool.self, forKey: "autologin")) ?? true
|
||||||
|
self.exceptions = (try? container.decodeIfPresent([WebBrowserException].self, forKey: "exceptions")) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||||
|
|
||||||
try container.encodeIfPresent(self.defaultWebBrowser, forKey: "defaultWebBrowser")
|
try container.encodeIfPresent(self.defaultWebBrowser, forKey: "defaultWebBrowser")
|
||||||
|
try container.encode(self.autologin, forKey: "autologin")
|
||||||
|
try container.encode(self.exceptions, forKey: "exceptions")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: WebBrowserSettings, rhs: WebBrowserSettings) -> Bool {
|
public static func ==(lhs: WebBrowserSettings, rhs: WebBrowserSettings) -> Bool {
|
||||||
return lhs.defaultWebBrowser == rhs.defaultWebBrowser
|
if lhs.defaultWebBrowser != rhs.defaultWebBrowser {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.autologin != rhs.autologin {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.exceptions != rhs.exceptions {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withUpdatedDefaultWebBrowser(_ defaultWebBrowser: String?) -> WebBrowserSettings {
|
public func withUpdatedDefaultWebBrowser(_ defaultWebBrowser: String?) -> WebBrowserSettings {
|
||||||
return WebBrowserSettings(defaultWebBrowser: defaultWebBrowser)
|
return WebBrowserSettings(defaultWebBrowser: defaultWebBrowser, autologin: self.autologin, exceptions: self.exceptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withUpdatedAutologin(_ autologin: Bool) -> WebBrowserSettings {
|
||||||
|
return WebBrowserSettings(defaultWebBrowser: self.defaultWebBrowser, autologin: autologin, exceptions: self.exceptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withUpdatedExceptions(_ exceptions: [WebBrowserException]) -> WebBrowserSettings {
|
||||||
|
return WebBrowserSettings(defaultWebBrowser: self.defaultWebBrowser, autologin: self.autologin, exceptions: exceptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -713,12 +713,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
|||||||
|
|
||||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||||
let bold: MarkdownAttributeSet
|
let bold: MarkdownAttributeSet
|
||||||
|
var link = body
|
||||||
if savedMessages {
|
if savedMessages {
|
||||||
bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0), additionalAttributes: ["URL": ""])
|
bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0), additionalAttributes: ["URL": ""])
|
||||||
|
link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||||
} else {
|
} else {
|
||||||
bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||||
}
|
}
|
||||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
|
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural)
|
||||||
self.textNode.attributedText = attributedText
|
self.textNode.attributedText = attributedText
|
||||||
self.textNode.maximumNumberOfLines = 2
|
self.textNode.maximumNumberOfLines = 2
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user