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
8120dde68c
commit
606e33607a
@ -206,9 +206,9 @@ final class AddressBarContentComponent: Component {
|
||||
self.activated(true)
|
||||
if let textField = self.textField {
|
||||
textField.becomeFirstResponder()
|
||||
Queue.mainQueue().justDispatch {
|
||||
Queue.mainQueue().after(0.3, {
|
||||
textField.selectAll(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,7 +238,9 @@ final class AddressBarContentComponent: Component {
|
||||
|
||||
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
if let component = self.component {
|
||||
component.performAction.invoke(.navigateTo(explicitUrl(textField.text ?? "")))
|
||||
let finalUrl = explicitUrl(textField.text ?? "")
|
||||
// finalUrl = finalUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? finalUrl
|
||||
component.performAction.invoke(.navigateTo(finalUrl))
|
||||
}
|
||||
textField.endEditing(true)
|
||||
return false
|
||||
@ -273,6 +275,10 @@ final class AddressBarContentComponent: Component {
|
||||
var title: String = ""
|
||||
if let parsedUrl = URL(string: component.url) {
|
||||
title = parsedUrl.host ?? component.url
|
||||
if title.hasPrefix("www.") {
|
||||
title.removeSubrange(title.startIndex ..< title.index(title.startIndex, offsetBy: 4))
|
||||
}
|
||||
title = title.idnaDecoded ?? title
|
||||
}
|
||||
self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, transition: transition)
|
||||
|
||||
@ -333,6 +339,7 @@ final class AddressBarContentComponent: Component {
|
||||
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)))
|
||||
self.cancelButton.isUserInteractionEnabled = isActiveWithText
|
||||
|
||||
let textX: CGFloat = backgroundFrame.minX + sideInset
|
||||
let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height))
|
||||
|
@ -148,7 +148,7 @@ final class BrowserAddressListComponent: Component {
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
self.endEditing(true)
|
||||
self.window?.endEditing(true)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
|
@ -162,12 +162,14 @@ protocol BrowserContent: UIView {
|
||||
var present: (ViewController, Any?) -> Void { get set }
|
||||
var presentInGlobalOverlay: (ViewController) -> Void { get set }
|
||||
var getNavigationController: () -> NavigationController? { get set }
|
||||
var openAppUrl: (String) -> Void { get set }
|
||||
|
||||
var minimize: () -> Void { get set }
|
||||
var close: () -> Void { get set }
|
||||
|
||||
var onScrollingUpdate: (ContentScrollingUpdate) -> Void { get set }
|
||||
|
||||
func resetScrolling()
|
||||
|
||||
func reload()
|
||||
func stop()
|
||||
|
||||
|
@ -17,7 +17,6 @@ import ShareController
|
||||
import UndoUI
|
||||
import UrlEscaping
|
||||
|
||||
|
||||
final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
@ -37,6 +36,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate
|
||||
}
|
||||
|
||||
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
|
||||
var openAppUrl: (String) -> Void = { _ in }
|
||||
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
|
||||
var minimize: () -> Void = { }
|
||||
var close: () -> Void = { }
|
||||
@ -360,6 +360,10 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate
|
||||
}
|
||||
}
|
||||
|
||||
func resetScrolling() {
|
||||
self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4))
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
||||
// self.currentError = nil
|
||||
self.updateFontState(self.currentFontState, force: true)
|
||||
|
@ -66,6 +66,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
||||
var currentAccessibilityAreas: [AccessibilityAreaNode] = []
|
||||
|
||||
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
|
||||
var openAppUrl: (String) -> Void = { _ in }
|
||||
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
|
||||
var minimize: () -> Void = { }
|
||||
var close: () -> Void = { }
|
||||
@ -766,6 +767,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
||||
self.readingProgress.set(readingProgress)
|
||||
}
|
||||
|
||||
func resetScrolling() {
|
||||
self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4))
|
||||
}
|
||||
|
||||
private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint {
|
||||
var contentOffset = CGPoint()
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
|
@ -35,6 +35,7 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
||||
let readingProgress: CGFloat
|
||||
let loadingProgress: Double?
|
||||
let collapseFraction: CGFloat
|
||||
let activate: () -> Void
|
||||
|
||||
init(
|
||||
backgroundColor: UIColor,
|
||||
@ -50,7 +51,8 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
||||
centerItem: AnyComponentWithIdentity<BrowserNavigationBarEnvironment>?,
|
||||
readingProgress: CGFloat,
|
||||
loadingProgress: Double?,
|
||||
collapseFraction: CGFloat
|
||||
collapseFraction: CGFloat,
|
||||
activate: @escaping () -> Void
|
||||
) {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.separatorColor = separatorColor
|
||||
@ -66,6 +68,7 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
||||
self.readingProgress = readingProgress
|
||||
self.loadingProgress = loadingProgress
|
||||
self.collapseFraction = collapseFraction
|
||||
self.activate = activate
|
||||
}
|
||||
|
||||
static func ==(lhs: BrowserNavigationBarComponent, rhs: BrowserNavigationBarComponent) -> Bool {
|
||||
@ -122,6 +125,7 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
||||
let leftItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
||||
let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
||||
let centerItems = ChildMap(environment: BrowserNavigationBarEnvironment.self, keyedBy: AnyHashable.self)
|
||||
let activate = Child(Button.self)
|
||||
|
||||
return { context in
|
||||
var availableWidth = context.availableSize.width
|
||||
@ -266,6 +270,23 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
||||
)
|
||||
}
|
||||
|
||||
if context.component.collapseFraction == 1.0 {
|
||||
let activateAction = context.component.activate
|
||||
let activate = activate.update(
|
||||
component: Button(
|
||||
content: AnyComponent(Rectangle(color: UIColor(rgb: 0x000000, alpha: 0.001))),
|
||||
action: {
|
||||
activateAction()
|
||||
}
|
||||
),
|
||||
availableSize: size,
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(activate
|
||||
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
}
|
||||
|
||||
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
|
||||
var openAppUrl: (String) -> Void = { _ in }
|
||||
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
|
||||
var minimize: () -> Void = { }
|
||||
var close: () -> Void = { }
|
||||
@ -352,6 +353,10 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
}
|
||||
}
|
||||
|
||||
func resetScrolling() {
|
||||
self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4))
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
||||
// self.currentError = nil
|
||||
self.updateFontState(self.currentFontState, force: true)
|
||||
|
@ -188,7 +188,10 @@ private final class BrowserScreenComponent: CombinedComponent {
|
||||
centerItem: navigationContent,
|
||||
readingProgress: context.component.contentState?.readingProgress ?? 0.0,
|
||||
loadingProgress: context.component.contentState?.estimatedProgress,
|
||||
collapseFraction: collapseFraction
|
||||
collapseFraction: collapseFraction,
|
||||
activate: {
|
||||
performAction.invoke(.expand)
|
||||
}
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
@ -267,6 +270,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
||||
)
|
||||
context.add(addressList
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0))
|
||||
.clipsToBounds(true)
|
||||
.appear(.default(alpha: true))
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
@ -314,6 +318,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
||||
case openAddressBar
|
||||
case closeAddressBar
|
||||
case navigateTo(String)
|
||||
case expand
|
||||
}
|
||||
|
||||
fileprivate final class Node: ViewControllerTracingNode {
|
||||
@ -568,6 +573,10 @@ public class BrowserScreen: ViewController, MinimizableController {
|
||||
updatedState.addressFocused = false
|
||||
return updatedState
|
||||
})
|
||||
case .expand:
|
||||
if let content = self.content.last {
|
||||
content.resetScrolling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -626,6 +635,14 @@ public class BrowserScreen: ViewController, MinimizableController {
|
||||
}
|
||||
self.pushContent(content, transition: .spring(duration: 0.4))
|
||||
}
|
||||
browserContent.openAppUrl = { [weak self] url in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: false, presentationData: self.presentationData, navigationController: self.controller?.navigationController as? NavigationController, dismissInput: { [weak self] in
|
||||
self?.view.window?.endEditing(true)
|
||||
})
|
||||
}
|
||||
browserContent.present = { [weak self] c, a in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
@ -989,6 +1006,10 @@ public class BrowserScreen: ViewController, MinimizableController {
|
||||
}
|
||||
}
|
||||
|
||||
if update.isReset {
|
||||
scrollingPanelOffsetFraction = 0.0
|
||||
}
|
||||
|
||||
if scrollingPanelOffsetFraction != self.scrollingPanelOffsetFraction {
|
||||
self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction
|
||||
self.requestLayout(transition: transition)
|
||||
|
@ -18,6 +18,7 @@ import UndoUI
|
||||
import LottieComponent
|
||||
import MultilineTextComponent
|
||||
import UrlEscaping
|
||||
import UrlHandling
|
||||
|
||||
private final class TonSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
private final class PendingTask {
|
||||
@ -139,6 +140,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
private let faviconDisposable = MetaDisposable()
|
||||
|
||||
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
|
||||
var openAppUrl: (String) -> Void = { _ in }
|
||||
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
|
||||
var minimize: () -> Void = { }
|
||||
var close: () -> Void = { }
|
||||
@ -201,6 +203,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.webView.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
|
||||
self.webView.allowsBackForwardNavigationGestures = true
|
||||
self.webView.scrollView.delegate = self
|
||||
self.webView.scrollView.clipsToBounds = false
|
||||
@ -214,7 +219,6 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil)
|
||||
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.hasOnlySecureContent), options: [], context: nil)
|
||||
if #available(iOS 15.0, *) {
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
}
|
||||
if #available(iOS 16.4, *) {
|
||||
@ -460,11 +464,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
ErrorComponent(
|
||||
theme: self.presentationData.theme,
|
||||
title: self.presentationData.strings.Browser_ErrorTitle,
|
||||
text: error.localizedDescription
|
||||
text: error.localizedDescription,
|
||||
insets: insets
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height)
|
||||
containerSize: CGSize(width: size.width, height: size.height)
|
||||
)
|
||||
if self.errorView.superview == nil {
|
||||
self.addSubview(self.errorView)
|
||||
@ -521,14 +526,25 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !decelerate {
|
||||
self.snapScrollingOffsetToInsets()
|
||||
|
||||
if self.ignoreUpdatesUntilScrollingStopped {
|
||||
self.ignoreUpdatesUntilScrollingStopped = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
self.snapScrollingOffsetToInsets()
|
||||
|
||||
if self.ignoreUpdatesUntilScrollingStopped {
|
||||
self.ignoreUpdatesUntilScrollingStopped = false
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) {
|
||||
guard !self.ignoreUpdatesUntilScrollingStopped else {
|
||||
return
|
||||
}
|
||||
let scrollView = self.webView.scrollView
|
||||
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
|
||||
if let previousScrollingOffsetValue = self.previousScrollingOffset {
|
||||
@ -558,6 +574,30 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
}
|
||||
}
|
||||
|
||||
private var ignoreUpdatesUntilScrollingStopped = false
|
||||
func resetScrolling() {
|
||||
self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4))
|
||||
self.ignoreUpdatesUntilScrollingStopped = true
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
if let url = navigationAction.request.url?.absoluteString {
|
||||
if isTelegramMeLink(url) || isTelegraPhLink(url) {
|
||||
decisionHandler(.cancel)
|
||||
self.minimize()
|
||||
self.openAppUrl(url)
|
||||
} else {
|
||||
if #available(iOS 14.5, *), navigationAction.shouldPerformDownload {
|
||||
decisionHandler(.download)
|
||||
} else {
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
||||
self.currentError = nil
|
||||
self.updateFontState(self.currentFontState, force: true)
|
||||
@ -881,15 +921,18 @@ private final class ErrorComponent: CombinedComponent {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let text: String
|
||||
let insets: UIEdgeInsets
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
title: String,
|
||||
text: String
|
||||
text: String,
|
||||
insets: UIEdgeInsets
|
||||
) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.insets = insets
|
||||
}
|
||||
|
||||
static func ==(lhs: ErrorComponent, rhs: ErrorComponent) -> Bool {
|
||||
@ -902,10 +945,14 @@ private final class ErrorComponent: CombinedComponent {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(Rectangle.self)
|
||||
let animation = Child(LottieComponent.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
@ -916,6 +963,17 @@ private final class ErrorComponent: CombinedComponent {
|
||||
let animationSpacing: CGFloat = 8.0
|
||||
let textSpacing: CGFloat = 8.0
|
||||
|
||||
let constrainedWidth = context.availableSize.width - 76.0 - context.component.insets.left - context.component.insets.right
|
||||
|
||||
let background = background.update(
|
||||
component: Rectangle(color: context.component.theme.list.plainBackgroundColor),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let animation = animation.update(
|
||||
component: LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "ChatListNoResults")
|
||||
@ -924,9 +982,6 @@ private final class ErrorComponent: CombinedComponent {
|
||||
availableSize: CGSize(width: animationSize, height: animationSize),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(animation
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + animation.size.height / 2.0))
|
||||
)
|
||||
contentHeight += animation.size.height + animationSpacing
|
||||
|
||||
let title = title.update(
|
||||
@ -939,12 +994,9 @@ private final class ErrorComponent: CombinedComponent {
|
||||
horizontalAlignment: .center
|
||||
),
|
||||
environment: {},
|
||||
availableSize: context.availableSize,
|
||||
availableSize: CGSize(width: constrainedWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0))
|
||||
)
|
||||
contentHeight += title.size.height + textSpacing
|
||||
|
||||
let text = text.update(
|
||||
@ -958,15 +1010,27 @@ private final class ErrorComponent: CombinedComponent {
|
||||
maximumNumberOfLines: 0
|
||||
),
|
||||
environment: {},
|
||||
availableSize: context.availableSize,
|
||||
availableSize: CGSize(width: constrainedWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(text
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + text.size.height / 2.0))
|
||||
)
|
||||
contentHeight += text.size.height
|
||||
|
||||
return CGSize(width: context.availableSize.width, height: contentHeight)
|
||||
|
||||
var originY = floor((context.availableSize.height - contentHeight) / 2.0)
|
||||
context.add(animation
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + animation.size.height / 2.0))
|
||||
)
|
||||
originY += animation.size.height + animationSpacing
|
||||
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + title.size.height / 2.0))
|
||||
)
|
||||
originY += title.size.height + textSpacing
|
||||
|
||||
context.add(text
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + text.size.height / 2.0))
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
317
submodules/BrowserUI/Sources/Punycode.swift
Normal file
317
submodules/BrowserUI/Sources/Punycode.swift
Normal file
@ -0,0 +1,317 @@
|
||||
//
|
||||
// Created by kojirof on 2018-11-19.
|
||||
// Copyright (c) 2018 Gumob. All rights reserved.
|
||||
//
|
||||
|
||||
//MIT License
|
||||
//
|
||||
//Copyright (c) 2018 Gumob
|
||||
//
|
||||
//Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
//of this software and associated documentation files (the "Software"), to deal
|
||||
//in the Software without restriction, including without limitation the rights
|
||||
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
//copies of the Software, and to permit persons to whom the Software is
|
||||
//furnished to do so, subject to the following conditions:
|
||||
//
|
||||
//The above copyright notice and this permission notice shall be included in all
|
||||
//copies or substantial portions of the Software.
|
||||
//
|
||||
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
//SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
public class Punycode {
|
||||
|
||||
/// Punycode RFC 3492
|
||||
/// See https://www.ietf.org/rfc/rfc3492.txt for standard details
|
||||
|
||||
private let base: Int = 36
|
||||
private let tMin: Int = 1
|
||||
private let tMax: Int = 26
|
||||
private let skew: Int = 38
|
||||
private let damp: Int = 700
|
||||
private let initialBias: Int = 72
|
||||
private let initialN: Int = 128
|
||||
|
||||
/// RFC 3492 specific
|
||||
private let delimiter: Character = "-"
|
||||
private let lowercase: ClosedRange<Character> = "a"..."z"
|
||||
private let digits: ClosedRange<Character> = "0"..."9"
|
||||
private let lettersBase: UInt32 = Character("a").unicodeScalars.first!.value
|
||||
private let digitsBase: UInt32 = Character("0").unicodeScalars.first!.value
|
||||
|
||||
/// IDNA
|
||||
private let ace: String = "xn--"
|
||||
|
||||
private func adaptBias(_ delta: Int, _ numberOfPoints: Int, _ firstTime: Bool) -> Int {
|
||||
var delta: Int = delta
|
||||
if firstTime {
|
||||
delta /= damp
|
||||
} else {
|
||||
delta /= 2
|
||||
}
|
||||
delta += delta / numberOfPoints
|
||||
var k: Int = 0
|
||||
while delta > ((base - tMin) * tMax) / 2 {
|
||||
delta /= base - tMin
|
||||
k += base
|
||||
}
|
||||
return k + ((base - tMin + 1) * delta) / (delta + skew)
|
||||
}
|
||||
|
||||
/// Maps a punycode character to index
|
||||
private func punycodeIndex(for character: Character) -> Int? {
|
||||
if lowercase.contains(character) {
|
||||
return Int(character.unicodeScalars.first!.value - lettersBase)
|
||||
} else if digits.contains(character) {
|
||||
return Int(character.unicodeScalars.first!.value - digitsBase) + 26 /// count of lowercase letters range
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps an index to corresponding punycode character
|
||||
private func punycodeValue(for digit: Int) -> Character? {
|
||||
guard digit < base else { return nil }
|
||||
if digit < 26 {
|
||||
return Character(UnicodeScalar(lettersBase.advanced(by: digit))!)
|
||||
} else {
|
||||
return Character(UnicodeScalar(digitsBase.advanced(by: digit - 26))!)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes punycode encoded string to original representation
|
||||
///
|
||||
/// - Parameter punycode: Punycode encoding (RFC 3492)
|
||||
/// - Returns: Decoded string or nil if the input cannot be decoded
|
||||
public func decodePunycode(_ punycode: Substring) -> String? {
|
||||
var n: Int = initialN
|
||||
var i: Int = 0
|
||||
var bias: Int = initialBias
|
||||
var output: [Character] = []
|
||||
var inputPosition = punycode.startIndex
|
||||
|
||||
let delimiterPosition: Substring.Index = punycode.lastIndex(of: delimiter) ?? punycode.startIndex
|
||||
if delimiterPosition > punycode.startIndex {
|
||||
output.append(contentsOf: punycode[..<delimiterPosition])
|
||||
inputPosition = punycode.index(after: delimiterPosition)
|
||||
}
|
||||
var punycodeInput: Substring = punycode[inputPosition..<punycode.endIndex]
|
||||
while !punycodeInput.isEmpty {
|
||||
let oldI: Int = i
|
||||
var w: Int = 1
|
||||
var k: Int = base
|
||||
repeat {
|
||||
let character: Character = punycodeInput.removeFirst()
|
||||
guard let digit: Int = punycodeIndex(for: character) else {
|
||||
return nil /// Failing on badly formatted punycode
|
||||
}
|
||||
i += digit * w
|
||||
let t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias)
|
||||
if digit < t {
|
||||
break
|
||||
}
|
||||
w *= base - t
|
||||
k += base
|
||||
} while !punycodeInput.isEmpty
|
||||
bias = adaptBias(i - oldI, output.count + 1, oldI == 0)
|
||||
n += i / (output.count + 1)
|
||||
i %= (output.count + 1)
|
||||
guard n >= 0x80, let scalar = UnicodeScalar(n) else {
|
||||
return nil
|
||||
}
|
||||
output.insert(Character(scalar), at: i)
|
||||
i += 1
|
||||
}
|
||||
|
||||
return String(output)
|
||||
}
|
||||
|
||||
/// Encodes string to punycode (RFC 3492)
|
||||
///
|
||||
/// - Parameter input: Input string
|
||||
/// - Returns: Punycode encoded string
|
||||
public func encodePunycode(_ input: Substring) -> String? {
|
||||
var n: Int = initialN
|
||||
var delta: Int = 0
|
||||
var bias: Int = initialBias
|
||||
var output: String = ""
|
||||
for scalar in input.unicodeScalars {
|
||||
if scalar.isASCII {
|
||||
let char = Character(scalar)
|
||||
output.append(char)
|
||||
} else if !scalar.isValid {
|
||||
return nil /// Encountered a scalar out of acceptable range
|
||||
}
|
||||
}
|
||||
var handled: Int = output.count
|
||||
let basic: Int = handled
|
||||
if basic > 0 {
|
||||
output.append(delimiter)
|
||||
}
|
||||
while handled < input.unicodeScalars.count {
|
||||
var minimumCodepoint: Int = 0x10FFFF
|
||||
for scalar: Unicode.Scalar in input.unicodeScalars {
|
||||
if scalar.value < minimumCodepoint && scalar.value >= n {
|
||||
minimumCodepoint = Int(scalar.value)
|
||||
}
|
||||
}
|
||||
delta += (minimumCodepoint - n) * (handled + 1)
|
||||
n = minimumCodepoint
|
||||
for scalar: Unicode.Scalar in input.unicodeScalars {
|
||||
if scalar.value < n {
|
||||
delta += 1
|
||||
} else if scalar.value == n {
|
||||
var q: Int = delta
|
||||
var k: Int = base
|
||||
while true {
|
||||
let t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias)
|
||||
if q < t {
|
||||
break
|
||||
}
|
||||
guard let character: Character = punycodeValue(for: t + ((q - t) % (base - t))) else { return nil }
|
||||
output.append(character)
|
||||
q = (q - t) / (base - t)
|
||||
k += base
|
||||
}
|
||||
guard let character: Character = punycodeValue(for: q) else { return nil }
|
||||
output.append(character)
|
||||
bias = adaptBias(delta, handled + 1, handled == basic)
|
||||
delta = 0
|
||||
handled += 1
|
||||
}
|
||||
}
|
||||
delta += 1
|
||||
n += 1
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/// Returns new string containing IDNA-encoded hostname
|
||||
///
|
||||
/// - Returns: IDNA encoded hostname or nil if the string can't be encoded
|
||||
public func encodeIDNA(_ input: Substring) -> String? {
|
||||
let parts: [Substring] = input.split(separator: ".")
|
||||
var output: String = ""
|
||||
for part: Substring in parts {
|
||||
if output.count > 0 {
|
||||
output.append(".")
|
||||
}
|
||||
if part.rangeOfCharacter(from: CharacterSet.urlHostAllowed.inverted) != nil {
|
||||
guard let encoded: String = part.lowercased().punycodeEncoded else { return nil }
|
||||
output += ace + encoded
|
||||
} else {
|
||||
output += part
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
/// Returns new string containing hostname decoded from IDNA representation
|
||||
///
|
||||
/// - Returns: Original hostname or nil if the string doesn't contain correct encoding
|
||||
public func decodedIDNA(_ input: Substring) -> String? {
|
||||
let parts: [Substring] = input.split(separator: ".")
|
||||
var output: String = ""
|
||||
for part: Substring in parts {
|
||||
if output.count > 0 {
|
||||
output.append(".")
|
||||
}
|
||||
if part.hasPrefix(ace) {
|
||||
guard let decoded: String = part.dropFirst(ace.count).punycodeDecoded else { return nil }
|
||||
output += decoded
|
||||
} else {
|
||||
output += part
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
private extension Substring {
|
||||
func lastIndex(of element: Character) -> String.Index? {
|
||||
var position: Index = endIndex
|
||||
while position > startIndex {
|
||||
position = self.index(before: position)
|
||||
if self[position] == element {
|
||||
return position
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private extension UnicodeScalar {
|
||||
var isValid: Bool {
|
||||
return value < 0xD880 || (value >= 0xE000 && value <= 0x1FFFFF)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Substring {
|
||||
/// Returns new string in punycode encoding (RFC 3492)
|
||||
///
|
||||
/// - Returns: Punycode encoded string or nil if the string can't be encoded
|
||||
var punycodeEncoded: String? {
|
||||
return Punycode().encodePunycode(self)
|
||||
}
|
||||
|
||||
/// Returns new string decoded from punycode representation (RFC 3492)
|
||||
///
|
||||
/// - Returns: Original string or nil if the string doesn't contain correct encoding
|
||||
var punycodeDecoded: String? {
|
||||
return Punycode().decodePunycode(self)
|
||||
}
|
||||
|
||||
/// Returns new string containing IDNA-encoded hostname
|
||||
///
|
||||
/// - Returns: IDNA encoded hostname or nil if the string can't be encoded
|
||||
var idnaEncoded: String? {
|
||||
return Punycode().encodeIDNA(self)
|
||||
}
|
||||
|
||||
/// Returns new string containing hostname decoded from IDNA representation
|
||||
///
|
||||
/// - Returns: Original hostname or nil if the string doesn't contain correct encoding
|
||||
var idnaDecoded: String? {
|
||||
return Punycode().decodedIDNA(self)
|
||||
}
|
||||
}
|
||||
|
||||
public extension String {
|
||||
|
||||
/// Returns new string in punycode encoding (RFC 3492)
|
||||
///
|
||||
/// - Returns: Punycode encoded string or nil if the string can't be encoded
|
||||
var punycodeEncoded: String? {
|
||||
return self[..<self.endIndex].punycodeEncoded
|
||||
}
|
||||
|
||||
/// Returns new string decoded from punycode representation (RFC 3492)
|
||||
///
|
||||
/// - Returns: Original string or nil if the string doesn't contain correct encoding
|
||||
var punycodeDecoded: String? {
|
||||
return self[..<self.endIndex].punycodeDecoded
|
||||
}
|
||||
|
||||
/// Returns new string containing IDNA-encoded hostname
|
||||
///
|
||||
/// - Returns: IDNA encoded hostname or nil if the string can't be encoded
|
||||
var idnaEncoded: String? {
|
||||
return self[..<self.endIndex].idnaEncoded
|
||||
}
|
||||
|
||||
/// Returns new string containing hostname decoded from IDNA representation
|
||||
///
|
||||
/// - Returns: Original hostname or nil if the string doesn't contain correct encoding
|
||||
var idnaDecoded: String? {
|
||||
return self[..<self.endIndex].idnaDecoded
|
||||
}
|
||||
}
|
@ -9,6 +9,11 @@ var uiWebview_SearchResultCount = 0;
|
||||
keyword - string to search
|
||||
*/
|
||||
|
||||
function isElementVisible(element) {
|
||||
var style = window.getComputedStyle(element);
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
||||
}
|
||||
|
||||
function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) {
|
||||
if (element) {
|
||||
if (element.nodeType == 3) { // Text node
|
||||
@ -19,6 +24,8 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) {
|
||||
var idx = value.toLowerCase().indexOf(keyword);
|
||||
|
||||
if (idx < 0) break;
|
||||
|
||||
// if (!isElementVisible(element)) break;
|
||||
|
||||
count++;
|
||||
elementTmp = document.createTextNode(value.substr(idx+keyword.length));
|
||||
@ -86,7 +93,7 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) {
|
||||
|
||||
|
||||
} else if (element.nodeType == 1) { // Element node
|
||||
if (element.style.display != "none" && element.nodeName.toLowerCase() != 'select') {
|
||||
if (element.nodeName.toLowerCase() != 'select') {
|
||||
for (var i=element.childNodes.length-1; i>=0; i--) {
|
||||
uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword);
|
||||
}
|
||||
|
@ -1008,11 +1008,12 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
||||
return
|
||||
}
|
||||
|
||||
let urlScheme = (parsedUrl.scheme ?? "").lowercased()
|
||||
var isInternetUrl = false
|
||||
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
|
||||
if ["http", "https"].contains(urlScheme) {
|
||||
isInternetUrl = true
|
||||
}
|
||||
if parsedUrl.scheme == "tonsite" {
|
||||
if urlScheme == "tonsite" {
|
||||
isInternetUrl = true
|
||||
}
|
||||
|
||||
@ -1032,7 +1033,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
||||
settings = .defaultSettings
|
||||
}
|
||||
if accessChallengeData.data.isLockable {
|
||||
if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil {
|
||||
if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == "inApp" {
|
||||
settings = WebBrowserSettings(defaultWebBrowser: "safari", exceptions: [])
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user