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.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(
|
||||
Button(
|
||||
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: {
|
||||
performAction.invoke(.close)
|
||||
@ -130,7 +133,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
||||
content: LottieComponent.AppBundleContent(
|
||||
name: "anim_moredots"
|
||||
),
|
||||
color: environment.theme.rootController.navigationBar.primaryTextColor,
|
||||
color: environment.theme.rootController.navigationBar.accentTextColor,
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
),
|
||||
@ -150,7 +153,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
||||
ReferenceButtonComponent(
|
||||
content: AnyComponent(
|
||||
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
|
||||
)
|
||||
),
|
||||
@ -211,6 +214,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
||||
id: "navigation",
|
||||
component: AnyComponent(
|
||||
NavigationToolbarContentComponent(
|
||||
accentColor: environment.theme.rootController.navigationBar.accentTextColor,
|
||||
textColor: environment.theme.rootController.navigationBar.primaryTextColor,
|
||||
canGoBack: context.component.contentState?.canGoBack ?? false,
|
||||
canGoForward: context.component.contentState?.canGoForward ?? false,
|
||||
@ -281,6 +285,8 @@ public class BrowserScreen: ViewController, MinimizableController {
|
||||
case increaseFontSize
|
||||
case resetFontSize
|
||||
case updateFontIsSerif(Bool)
|
||||
case addBookmark
|
||||
case openBookmarks
|
||||
}
|
||||
|
||||
fileprivate final class Node: ViewControllerTracingNode {
|
||||
@ -502,6 +508,12 @@ public class BrowserScreen: ViewController, MinimizableController {
|
||||
return updatedState
|
||||
})
|
||||
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))
|
||||
}
|
||||
|
||||
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() {
|
||||
for content in self.content {
|
||||
content.onScrollingUpdate = { _ in }
|
||||
@ -777,7 +826,11 @@ public class BrowserScreen: ViewController, MinimizableController {
|
||||
performAction.invoke(.updateSearchActive(true))
|
||||
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 {
|
||||
self.context.sharedContext.applicationBindings.openUrl(openInUrl)
|
||||
}
|
||||
@ -1074,7 +1127,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
public var minimizedProgress: Float? {
|
||||
if let contentState = self.node.contentState {
|
||||
return Float(contentState.readingProgress)
|
||||
|
@ -33,7 +33,7 @@ final class SearchBarContentComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView, UITextFieldDelegate {
|
||||
private final class EmojiSearchTextField: UITextField {
|
||||
private final class SearchTextField: UITextField {
|
||||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return bounds.integral
|
||||
}
|
||||
@ -75,7 +75,7 @@ final class SearchBarContentComponent: Component {
|
||||
private var placeholderContent = ComponentView<Empty>()
|
||||
|
||||
private var textFrame: CGRect?
|
||||
private var textField: EmojiSearchTextField?
|
||||
private var textField: SearchTextField?
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
@ -160,7 +160,7 @@ final class SearchBarContentComponent: Component {
|
||||
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 = EmojiSearchTextField(frame: textFieldFrame)
|
||||
let textField = SearchTextField(frame: textFieldFrame)
|
||||
textField.autocorrectionType = .no
|
||||
textField.returnKeyType = .search
|
||||
self.textField = textField
|
||||
@ -285,7 +285,7 @@ final class SearchBarContentComponent: Component {
|
||||
component: AnyComponent(Text(
|
||||
text: strings.Common_Cancel,
|
||||
font: Font.regular(17.0),
|
||||
color: theme.rootController.navigationBar.primaryTextColor
|
||||
color: theme.rootController.navigationBar.accentTextColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
||||
|
@ -120,6 +120,7 @@ final class BrowserToolbarComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
let accentColor: UIColor
|
||||
let textColor: UIColor
|
||||
let canGoBack: Bool
|
||||
let canGoForward: Bool
|
||||
@ -127,12 +128,14 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
||||
|
||||
init(
|
||||
accentColor: UIColor,
|
||||
textColor: UIColor,
|
||||
canGoBack: Bool,
|
||||
canGoForward: Bool,
|
||||
performAction: ActionSlot<BrowserScreen.Action>,
|
||||
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
||||
) {
|
||||
self.accentColor = accentColor
|
||||
self.textColor = textColor
|
||||
self.canGoBack = canGoBack
|
||||
self.canGoForward = canGoForward
|
||||
@ -141,6 +144,9 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool {
|
||||
if lhs.accentColor != rhs.accentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
@ -157,6 +163,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
let back = Child(ContextReferenceButtonComponent.self)
|
||||
let forward = Child(ContextReferenceButtonComponent.self)
|
||||
let share = Child(Button.self)
|
||||
let bookmark = Child(Button.self)
|
||||
let openIn = Child(Button.self)
|
||||
|
||||
return { context in
|
||||
@ -166,7 +173,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
|
||||
let sideInset: CGFloat = 5.0
|
||||
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 back = back.update(
|
||||
@ -174,7 +181,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
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,
|
||||
@ -202,7 +209,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
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,
|
||||
@ -229,7 +236,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Chat List/NavigationShare",
|
||||
tintColor: context.component.textColor
|
||||
tintColor: context.component.accentColor
|
||||
)
|
||||
),
|
||||
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))
|
||||
)
|
||||
|
||||
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(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Minimize",
|
||||
tintColor: context.component.textColor
|
||||
name: "Instant View/Browser",
|
||||
tintColor: context.component.accentColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.minimize)
|
||||
performAction.invoke(.openIn)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
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
|
||||
|
@ -204,9 +204,9 @@ private final class VideoRecorderImpl {
|
||||
|
||||
if let videoInput = self.videoInput {
|
||||
let time = CACurrentMediaTime()
|
||||
if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime {
|
||||
print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)")
|
||||
}
|
||||
// if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime {
|
||||
// print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)")
|
||||
// }
|
||||
self.previousPresentationTime = presentationTime.seconds
|
||||
self.previousAppendTime = time
|
||||
|
||||
|
@ -80,7 +80,6 @@ public final class LocationViewController: ViewController {
|
||||
private let isStoryLocation: Bool
|
||||
|
||||
private let locationManager = LocationManager()
|
||||
private var permissionDisposable: Disposable?
|
||||
|
||||
private var interaction: LocationViewInteraction?
|
||||
|
||||
|
@ -100,9 +100,9 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
case useLessVoiceData(PresentationTheme, String, Bool)
|
||||
case useLessVoiceDataInfo(PresentationTheme, String)
|
||||
case otherHeader(PresentationTheme, String)
|
||||
case openLinksIn(PresentationTheme, String, String)
|
||||
case shareSheet(PresentationTheme, String)
|
||||
case saveEditedPhotos(PresentationTheme, String, Bool)
|
||||
case openLinksIn(PresentationTheme, String, String)
|
||||
case pauseMusicOnRecording(PresentationTheme, String, Bool)
|
||||
case raiseToListen(PresentationTheme, String, Bool)
|
||||
case raiseToListenInfo(PresentationTheme, String)
|
||||
@ -123,7 +123,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
return DataAndStorageSection.backgroundDownload.rawValue
|
||||
case .useLessVoiceData, .useLessVoiceDataInfo:
|
||||
return DataAndStorageSection.voiceCalls.rawValue
|
||||
case .otherHeader, .shareSheet, .saveEditedPhotos, .openLinksIn, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo:
|
||||
case .otherHeader, .openLinksIn, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo:
|
||||
return DataAndStorageSection.other.rawValue
|
||||
case .connectionHeader, .connectionProxy:
|
||||
return DataAndStorageSection.connection.rawValue
|
||||
@ -162,11 +162,11 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
return 24
|
||||
case .otherHeader:
|
||||
return 29
|
||||
case .shareSheet:
|
||||
return 30
|
||||
case .saveEditedPhotos:
|
||||
return 31
|
||||
case .openLinksIn:
|
||||
return 30
|
||||
case .shareSheet:
|
||||
return 31
|
||||
case .saveEditedPhotos:
|
||||
return 32
|
||||
case .pauseMusicOnRecording:
|
||||
return 33
|
||||
@ -257,6 +257,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
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):
|
||||
if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
@ -269,12 +275,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
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):
|
||||
if case let .pauseMusicOnRecording(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
@ -386,6 +386,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .otherHeader(_, text):
|
||||
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):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
|
||||
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
|
||||
arguments.toggleSaveEditedPhotos(value)
|
||||
}, 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):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.togglePauseMusicOnRecording(value)
|
||||
@ -618,11 +618,11 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
|
||||
entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription))
|
||||
|
||||
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, *) {
|
||||
entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings))
|
||||
}
|
||||
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(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak))
|
||||
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 AccountContext
|
||||
import OpenInExternalAppUI
|
||||
import ItemListPeerActionItem
|
||||
import UndoUI
|
||||
import WebKit
|
||||
import LinkPresentation
|
||||
|
||||
private final class WebBrowserSettingsControllerArguments {
|
||||
let context: AccountContext
|
||||
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.updateDefaultBrowser = updateDefaultBrowser
|
||||
self.updateAutologin = updateAutologin
|
||||
self.clearCookies = clearCookies
|
||||
self.addException = addException
|
||||
self.clearExceptions = clearExceptions
|
||||
}
|
||||
}
|
||||
|
||||
private enum WebBrowserSettingsSection: Int32 {
|
||||
case browsers
|
||||
case autologin
|
||||
case clearCookies
|
||||
case exceptions
|
||||
}
|
||||
|
||||
private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
||||
case browserHeader(PresentationTheme, String)
|
||||
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 {
|
||||
switch self {
|
||||
case .browserHeader, .browser:
|
||||
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
|
||||
case let .browser(_, _, _, _, _, 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 {
|
||||
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) {
|
||||
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] = []
|
||||
|
||||
let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org"))
|
||||
|
||||
entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_DefaultBrowser))
|
||||
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, selectedBrowser == nil, 0))
|
||||
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_InAppSafari, .safari, "inApp", selectedBrowser == "inApp", 1))
|
||||
entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title))
|
||||
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, settings.defaultWebBrowser == nil, 0))
|
||||
|
||||
var index: Int32 = 2
|
||||
var index: Int32 = 1
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
public func webBrowserSettingsController(context: AccountContext) -> ViewController {
|
||||
let arguments = WebBrowserSettingsControllerArguments(context: context, updateDefaultBrowser: { identifier in
|
||||
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { $0.withUpdatedDefaultWebBrowser(identifier) }).start()
|
||||
})
|
||||
var clearCookiesImpl: (() -> Void)?
|
||||
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
|
||||
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
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 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))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 itemListPlusIcon
|
||||
case itemListRoundPlusIcon
|
||||
case itemListAccentDeleteIcon
|
||||
case itemListDeleteIcon
|
||||
case itemListDeleteIndicatorIcon
|
||||
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? {
|
||||
return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in
|
||||
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)
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
trackOffsetUpdated: { _, _, _ in
|
||||
|
@ -48,6 +48,7 @@ import StickerPackEditTitleController
|
||||
import StickerPickerScreen
|
||||
import UIKitRuntimeUtils
|
||||
import ImageObjectSeparation
|
||||
import DeviceAccess
|
||||
|
||||
private let playbackButtonTag = GenericComponentViewTag()
|
||||
private let muteButtonTag = GenericComponentViewTag()
|
||||
@ -2553,6 +2554,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
var recording: MediaEditorScreen.Recording
|
||||
|
||||
private let locationManager = LocationManager()
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
@ -4584,7 +4587,37 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
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
|
||||
var currentWeatherCount = 0
|
||||
self.entitiesView.eachView { entityView in
|
||||
@ -4948,9 +4981,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if let self {
|
||||
if let weatherPromise = self.weatherPromise {
|
||||
let _ = (weatherPromise.get()
|
||||
|> take(1)).start(next: { [weak self] weather in
|
||||
if let self, case let .loaded(loaded) = weather {
|
||||
self.addWeather(loaded)
|
||||
|> take(1)).start(next: { [weak self] result in
|
||||
if let self {
|
||||
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 positionUpdated: (Double, Bool) -> Void
|
||||
let coverPositionUpdated: (Double, Bool, @escaping () -> Void) -> Void
|
||||
let trackTrimUpdated: (Int32, Double, Double, Bool, Bool) -> Void
|
||||
let trackOffsetUpdated: (Int32, Double, Bool) -> Void
|
||||
let trackLongPressed: (Int32, UIView) -> Void
|
||||
@ -104,6 +105,7 @@ public final class MediaScrubberComponent: Component {
|
||||
tracks: [Track],
|
||||
portalView: PortalView? = nil,
|
||||
positionUpdated: @escaping (Double, Bool) -> Void,
|
||||
coverPositionUpdated: @escaping (Double, Bool, @escaping () -> Void) -> Void = { _, _, _ in },
|
||||
trackTrimUpdated: @escaping (Int32, Double, Double, Bool, Bool) -> Void,
|
||||
trackOffsetUpdated: @escaping (Int32, Double, Bool) -> Void,
|
||||
trackLongPressed: @escaping (Int32, UIView) -> Void
|
||||
@ -119,6 +121,7 @@ public final class MediaScrubberComponent: Component {
|
||||
self.tracks = tracks
|
||||
self.portalView = portalView
|
||||
self.positionUpdated = positionUpdated
|
||||
self.coverPositionUpdated = coverPositionUpdated
|
||||
self.trackTrimUpdated = trackTrimUpdated
|
||||
self.trackOffsetUpdated = trackOffsetUpdated
|
||||
self.trackLongPressed = trackLongPressed
|
||||
@ -164,6 +167,7 @@ public final class MediaScrubberComponent: Component {
|
||||
|
||||
private var selectedTrackId: Int32 = 0
|
||||
private var isPanningCursor = false
|
||||
private var ignoreCursorPositionUpdate = false
|
||||
|
||||
private var scrubberSize: CGSize?
|
||||
|
||||
@ -327,10 +331,18 @@ public final class MediaScrubberComponent: Component {
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
self.isPanningCursor = true
|
||||
component.positionUpdated(position, false)
|
||||
if case .cover = component.style {
|
||||
component.coverPositionUpdated(position, false, {})
|
||||
} else {
|
||||
component.positionUpdated(position, false)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
self.isPanningCursor = false
|
||||
component.positionUpdated(position, true)
|
||||
if case .cover = component.style {
|
||||
component.coverPositionUpdated(position, false, {})
|
||||
} else {
|
||||
component.positionUpdated(position, true)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -345,7 +357,7 @@ public final class MediaScrubberComponent: Component {
|
||||
var y: CGFloat = -5.0 - UIScreenPixel
|
||||
if let component = self.component, case .cover = component.style {
|
||||
cursorWidth = 30.0 + 12.0
|
||||
cursorMargin = 0.0
|
||||
cursorMargin = handleWidth
|
||||
height = 50.0
|
||||
isCover = true
|
||||
y += 1.0
|
||||
@ -472,6 +484,23 @@ public final class MediaScrubberComponent: Component {
|
||||
} else {
|
||||
trackTransition = .immediate
|
||||
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
|
||||
guard let self else {
|
||||
return
|
||||
@ -659,13 +688,15 @@ public final class MediaScrubberComponent: Component {
|
||||
self.cursorPositionAnimation = nil
|
||||
self.cursorDisplayLink?.isPaused = true
|
||||
|
||||
var cursorPosition = component.position
|
||||
if let offset = self.mainAudioTrackOffset {
|
||||
cursorPosition -= offset
|
||||
if !self.ignoreCursorPositionUpdate {
|
||||
var cursorPosition = component.position
|
||||
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 {
|
||||
if let (_, _, end, ended) = self.cursorPositionAnimation {
|
||||
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] = []
|
||||
|
||||
var onSelection: (Int32) -> Void = { _ in }
|
||||
var onTap: (CGFloat) -> Void = { _ in }
|
||||
var offsetUpdated: (Double, Bool) -> Void = { _, _ in }
|
||||
var updated: (ComponentTransition) -> Void = { _ in }
|
||||
|
||||
@ -794,10 +826,15 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard let (track, _, _, _) = self.params else {
|
||||
guard let params = self.params else {
|
||||
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) {
|
||||
@ -841,6 +878,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
||||
}
|
||||
|
||||
private var params: (
|
||||
style: MediaScrubberComponent.Style,
|
||||
track: MediaScrubberComponent.Track,
|
||||
isSelected: Bool,
|
||||
availableSize: CGSize,
|
||||
@ -889,7 +927,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
||||
transition: ComponentTransition
|
||||
) -> CGSize {
|
||||
let previousParams = self.params
|
||||
self.params = (track, isSelected, availableSize, duration)
|
||||
self.params = (style, track, isSelected, availableSize, duration)
|
||||
|
||||
let fullTrackHeight: CGFloat
|
||||
let framesCornerRadius: CGFloat
|
||||
|
@ -2014,7 +2014,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
hasCategories = true
|
||||
}
|
||||
let sendAsPeersCount = component.stateContext.stateValue?.sendAsPeers.count ?? 1
|
||||
if sendAsPeersCount > 1 && !"".isEmpty {
|
||||
if sendAsPeersCount > 1 {
|
||||
hasChannels = true
|
||||
}
|
||||
if let currentHasChannels = self.currentHasChannels, currentHasChannels != hasChannels {
|
||||
@ -2618,7 +2618,11 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
||||
} else {
|
||||
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
|
||||
} else {
|
||||
if hasChannels {
|
||||
|
@ -81,7 +81,11 @@ struct CameraState: Equatable {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -408,11 +412,11 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
||||
let isFirstRecording = initialDuration.isZero
|
||||
controller.node.resumeCameraCapture()
|
||||
|
||||
controller.updatePreviewState({ _ in return nil}, transition: .spring(duration: 0.4))
|
||||
|
||||
controller.node.dismissAllTooltips()
|
||||
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) {
|
||||
Queue.mainQueue().after(0.15) {
|
||||
self.resultDisposable.set((camera.startRecording()
|
||||
@ -438,6 +442,10 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
||||
if initialDuration > 0.0 {
|
||||
controller.onResume()
|
||||
}
|
||||
|
||||
if controller.cameraState.position == .front && controller.cameraState.flashMode == .on {
|
||||
self.updateScreenBrightness()
|
||||
}
|
||||
}
|
||||
|
||||
func stopVideoRecording() {
|
||||
@ -645,7 +653,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
||||
action: { [weak state] in
|
||||
if let state {
|
||||
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" : [
|
||||
{
|
||||
"filename" : "ic_lt_safari.pdf",
|
||||
"filename" : "Bookmark.pdf",
|
||||
"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" : [
|
||||
{
|
||||
"filename" : "cross.pdf",
|
||||
"filename" : "Close.pdf",
|
||||
"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 passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil {
|
||||
settings = WebBrowserSettings(defaultWebBrowser: "safari")
|
||||
settings = WebBrowserSettings(defaultWebBrowser: "safari", autologin: false, exceptions: [])
|
||||
}
|
||||
}
|
||||
return settings
|
||||
|
@ -3,35 +3,84 @@ import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
public struct WebBrowserSettings: Codable, Equatable {
|
||||
public let defaultWebBrowser: String?
|
||||
public struct WebBrowserException: Codable, Equatable {
|
||||
public let domain: String
|
||||
public let title: String
|
||||
|
||||
public static var defaultSettings: WebBrowserSettings {
|
||||
return WebBrowserSettings(defaultWebBrowser: nil)
|
||||
public init(domain: String, title: String) {
|
||||
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.autologin = autologin
|
||||
self.exceptions = exceptions
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
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 {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 bold: MarkdownAttributeSet
|
||||
var link = body
|
||||
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": ""])
|
||||
link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
} else {
|
||||
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.maximumNumberOfLines = 2
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user