Initial in-app browser implementation

This commit is contained in:
Ilya Laktyushin 2023-02-18 00:59:18 +04:00
parent fa0130bf8c
commit bc1930be7c
16 changed files with 2903 additions and 1425 deletions

View File

@ -20,6 +20,12 @@ swift_library(
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/InstantPageUI:InstantPageUI", "//submodules/InstantPageUI:InstantPageUI",
"//submodules/ContextUI:ContextUI", "//submodules/ContextUI:ContextUI",
"//submodules/UndoUI:UndoUI",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -1,51 +1,86 @@
import Foundation import Foundation
import UIKit import UIKit
import AsyncDisplayKit import ComponentFlow
import Display
import SwiftSignalKit import SwiftSignalKit
final class BrowserContentState { final class BrowserContentState: Equatable {
enum ContentType: Equatable {
case webPage
case instantPage
}
let title: String let title: String
let url: String let url: String
let estimatedProgress: Double let estimatedProgress: Double
let isInstant: Bool let contentType: ContentType
var canGoBack: Bool var canGoBack: Bool
var canGoForward: Bool var canGoForward: Bool
init(title: String, url: String, estimatedProgress: Double, isInstant: Bool, canGoBack: Bool = false, canGoForward: Bool = false) { init(
title: String,
url: String,
estimatedProgress: Double,
contentType: ContentType,
canGoBack: Bool = false,
canGoForward: Bool = false
) {
self.title = title self.title = title
self.url = url self.url = url
self.estimatedProgress = estimatedProgress self.estimatedProgress = estimatedProgress
self.isInstant = isInstant self.contentType = contentType
self.canGoBack = canGoBack self.canGoBack = canGoBack
self.canGoForward = canGoForward self.canGoForward = canGoForward
} }
static func == (lhs: BrowserContentState, rhs: BrowserContentState) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.url != rhs.url {
return false
}
if lhs.estimatedProgress != rhs.estimatedProgress {
return false
}
if lhs.contentType != rhs.contentType {
return false
}
if lhs.canGoBack != rhs.canGoBack {
return false
}
if lhs.canGoForward != rhs.canGoForward {
return false
}
return true
}
func withUpdatedTitle(_ title: String) -> BrowserContentState { func withUpdatedTitle(_ title: String) -> BrowserContentState {
return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, isInstant: self.isInstant, canGoBack: self.canGoBack, canGoForward: self.canGoForward) return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward)
} }
func withUpdatedUrl(_ url: String) -> BrowserContentState { func withUpdatedUrl(_ url: String) -> BrowserContentState {
return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, isInstant: self.isInstant, canGoBack: self.canGoBack, canGoForward: self.canGoForward) return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward)
} }
func withUpdatedEstimatedProgress(_ estimatedProgress: Double) -> BrowserContentState { func withUpdatedEstimatedProgress(_ estimatedProgress: Double) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, isInstant: self.isInstant, canGoBack: self.canGoBack, canGoForward: self.canGoForward) return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward)
} }
func withUpdatedCanGoBack(_ canGoBack: Bool) -> BrowserContentState { func withUpdatedCanGoBack(_ canGoBack: Bool) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, isInstant: self.isInstant, canGoBack: canGoBack, canGoForward: self.canGoForward) return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: canGoBack, canGoForward: self.canGoForward)
} }
func withUpdatedCanGoForward(_ canGoForward: Bool) -> BrowserContentState { func withUpdatedCanGoForward(_ canGoForward: Bool) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, isInstant: self.isInstant, canGoBack: self.canGoBack, canGoForward: canGoForward) return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: canGoForward)
} }
} }
protocol BrowserContent: ASDisplayNode { protocol BrowserContent: UIView {
var state: Signal<BrowserContentState, NoError> { get } var state: Signal<BrowserContentState, NoError> { get }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void { get set }
func navigateBack() func navigateBack()
func navigateForward() func navigateForward()
@ -58,5 +93,30 @@ protocol BrowserContent: ASDisplayNode {
func scrollToTop() func scrollToTop()
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: Transition)
}
struct ContentScrollingUpdate {
public var relativeOffset: CGFloat
public var absoluteOffsetToTopEdge: CGFloat?
public var absoluteOffsetToBottomEdge: CGFloat?
public var isReset: Bool
public var isInteracting: Bool
public var transition: Transition
public init(
relativeOffset: CGFloat,
absoluteOffsetToTopEdge: CGFloat?,
absoluteOffsetToBottomEdge: CGFloat?,
isReset: Bool,
isInteracting: Bool,
transition: Transition
) {
self.relativeOffset = relativeOffset
self.absoluteOffsetToTopEdge = absoluteOffsetToTopEdge
self.absoluteOffsetToBottomEdge = absoluteOffsetToBottomEdge
self.isReset = isReset
self.isInteracting = isInteracting
self.transition = transition
}
} }

View File

@ -7,12 +7,12 @@ import AppBundle
import ContextUI import ContextUI
final class BrowserFontSizeContextMenuItem: ContextMenuCustomItem { final class BrowserFontSizeContextMenuItem: ContextMenuCustomItem {
private let value: CGFloat private let value: Int32
private let decrease: () -> CGFloat private let decrease: () -> Int32
private let increase: () -> CGFloat private let increase: () -> Int32
private let reset: () -> Void private let reset: () -> Void
init(value: CGFloat, decrease: @escaping () -> CGFloat, increase: @escaping () -> CGFloat, reset: @escaping () -> Void) { init(value: Int32, decrease: @escaping () -> Int32, increase: @escaping () -> Int32, reset: @escaping () -> Void) {
self.value = value self.value = value
self.decrease = decrease self.decrease = decrease
self.increase = increase self.increase = increase
@ -46,17 +46,17 @@ private final class BrowserFontSizeContextMenuItemNode: ASDisplayNode, ContextMe
private let leftSeparatorNode: ASDisplayNode private let leftSeparatorNode: ASDisplayNode
private let rightSeparatorNode: ASDisplayNode private let rightSeparatorNode: ASDisplayNode
var value: CGFloat = 1.0 { var value: Int32 = 100 {
didSet { didSet {
self.updateValue() self.updateValue()
} }
} }
private let decrease: () -> CGFloat private let decrease: () -> Int32
private let increase: () -> CGFloat private let increase: () -> Int32
private let reset: () -> Void private let reset: () -> Void
init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, value: CGFloat, decrease: @escaping () -> CGFloat, increase: @escaping () -> CGFloat, reset: @escaping () -> Void) { init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, value: Int32, decrease: @escaping () -> Int32, increase: @escaping () -> Int32, reset: @escaping () -> Void) {
self.presentationData = presentationData self.presentationData = presentationData
self.value = value self.value = value
self.decrease = decrease self.decrease = decrease
@ -198,14 +198,14 @@ private final class BrowserFontSizeContextMenuItemNode: ASDisplayNode, ContextMe
} }
private func updateValue() { private func updateValue() {
self.centerTextNode.attributedText = NSAttributedString(string: "\(Int(self.value * 100.0))%", font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) self.centerTextNode.attributedText = NSAttributedString(string: "\(self.value)%", font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)
let _ = self.centerTextNode.updateLayout(CGSize(width: 70.0, height: .greatestFiniteMagnitude)) let _ = self.centerTextNode.updateLayout(CGSize(width: 70.0, height: .greatestFiniteMagnitude))
self.leftButtonNode.isEnabled = self.value > 0.5 self.leftButtonNode.isEnabled = self.value > 50
self.leftIconNode.alpha = self.leftButtonNode.isEnabled ? 1.0 : 0.3 self.leftIconNode.alpha = self.leftButtonNode.isEnabled ? 1.0 : 0.3
self.rightButtonNode.isEnabled = self.value < 2.0 self.rightButtonNode.isEnabled = self.value < 150
self.rightIconNode.alpha = self.rightButtonNode.isEnabled ? 1.0 : 0.3 self.rightIconNode.alpha = self.rightButtonNode.isEnabled ? 1.0 : 0.3
self.centerButtonNode.isEnabled = self.value != 1.0 self.centerButtonNode.isEnabled = self.value != 100
self.centerTextNode.alpha = self.centerButtonNode.isEnabled ? 1.0 : 0.4 self.centerTextNode.alpha = self.centerButtonNode.isEnabled ? 1.0 : 0.4
} }
@ -255,7 +255,7 @@ private final class BrowserFontSizeContextMenuItemNode: ASDisplayNode, ContextMe
@objc private func centerPressed() { @objc private func centerPressed() {
self.reset() self.reset()
self.value = 1.0 self.value = 100
} }
func canBeHighlighted() -> Bool { func canBeHighlighted() -> Bool {

View File

@ -5,14 +5,877 @@
//import Postbox //import Postbox
//import SwiftSignalKit //import SwiftSignalKit
//import Display //import Display
//import ComponentFlow
//import TelegramPresentationData //import TelegramPresentationData
//import TelegramUIPreferences //import TelegramUIPreferences
//import AccountContext //import AccountContext
//import AppBundle //import AppBundle
//import InstantPageUI //import InstantPageUI
// //
//final class BrowserInstantPageContent: ASDisplayNode, BrowserContent { //final class InstantPageView: UIView, UIScrollViewDelegate {
// private let instantPageNode: InstantPageContentNode // private let webPage: TelegramMediaWebpage
// private var initialAnchor: String?
// private var pendingAnchor: String?
// private var initialState: InstantPageStoredState?
//
// private let scrollNode: ASScrollNode
// private let scrollNodeHeader: ASDisplayNode
// private let scrollNodeFooter: ASDisplayNode
// private var linkHighlightingNode: LinkHighlightingNode?
// private var textSelectionNode: LinkHighlightingNode?
//
// var currentLayout: InstantPageLayout?
// var currentLayoutTiles: [InstantPageTile] = []
// var currentLayoutItemsWithNodes: [InstantPageItem] = []
// var distanceThresholdGroupCount: [Int: Int] = [:]
//
// var visibleTiles: [Int: InstantPageTileNode] = [:]
// var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
//
// var currentWebEmbedHeights: [Int : CGFloat] = [:]
// var currentExpandedDetails: [Int : Bool]?
// var currentDetailsItems: [InstantPageDetailsItem] = []
//
// var currentAccessibilityAreas: [AccessibilityAreaNode] = []
//
// init(webPage: TelegramMediaWebpage) {
// self.webPage = webPage
//
// self.scrollNode = ASScrollNode()
//
// super.init(frame: frame)
//
// self.addSubview(self.scrollNode.view)
//
// self.scrollNode.view.delaysContentTouches = false
// self.scrollNode.view.delegate = self
//
// if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
// self.scrollNode.view.contentInsetAdjustmentBehavior = .never
// }
//
// let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
// recognizer.delaysTouchesBegan = false
// recognizer.tapActionAtPoint = { [weak self] point in
// if let strongSelf = self {
// return strongSelf.tapActionAtPoint(point)
// }
// return .waitForSingleTap
// }
// recognizer.highlight = { [weak self] point in
// if let strongSelf = self {
// strongSelf.updateTouchesAtPoint(point)
// }
// }
// self.scrollNode.view.addGestureRecognizer(recognizer)
// }
//
// required init?(coder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
// }
//
// func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction {
// if let currentLayout = self.currentLayout {
// for item in currentLayout.items {
// let frame = self.effectiveFrameForItem(item)
// if frame.contains(point) {
// if item is InstantPagePeerReferenceItem {
// return .fail
// } else if item is InstantPageAudioItem {
// return .fail
// } else if item is InstantPageArticleItem {
// return .fail
// } else if item is InstantPageFeedbackItem {
// return .fail
// } else if let item = item as? InstantPageDetailsItem {
// for (_, itemNode) in self.visibleItemsWithNodes {
// if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
// return itemNode.tapActionAtPoint(point.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY))
// }
// }
// }
// if !(item is InstantPageImageItem || item is InstantPagePlayableVideoItem) {
// break
// }
// }
// }
// }
// return .waitForSingleTap
// }
//
// private func updateTouchesAtPoint(_ location: CGPoint?) {
// var rects: [CGRect]?
// if let location = location, let currentLayout = self.currentLayout {
// for item in currentLayout.items {
// let itemFrame = self.effectiveFrameForItem(item)
// if itemFrame.contains(location) {
// var contentOffset = CGPoint()
// if let item = item as? InstantPageScrollableItem {
// contentOffset = self.scrollableContentOffset(item: item)
// }
// var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY))
//
// for i in 0 ..< itemRects.count {
// itemRects[i] = itemRects[i].offsetBy(dx: itemFrame.minX - contentOffset.x, dy: itemFrame.minY).insetBy(dx: -2.0, dy: -2.0)
// }
// if !itemRects.isEmpty {
// rects = itemRects
// break
// }
// }
// }
// }
//
// if let rects = rects {
// let linkHighlightingNode: LinkHighlightingNode
// if let current = self.linkHighlightingNode {
// linkHighlightingNode = current
// } else {
// let highlightColor = self.theme?.linkHighlightColor ?? UIColor(rgb: 0x007aff).withAlphaComponent(0.4)
// linkHighlightingNode = LinkHighlightingNode(color: highlightColor)
// linkHighlightingNode.isUserInteractionEnabled = false
// self.linkHighlightingNode = linkHighlightingNode
// self.scrollNode.addSubnode(linkHighlightingNode)
// }
// linkHighlightingNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
// linkHighlightingNode.updateRects(rects)
// } else if let linkHighlightingNode = self.linkHighlightingNode {
// self.linkHighlightingNode = nil
// linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
// linkHighlightingNode?.removeFromSupernode()
// })
// }
// }
//
// private func updatePageLayout() {
// guard let containerLayout = self.containerLayout, let webPage = self.webPage, let theme = self.theme else {
// return
// }
//
// let currentLayout = instantPageLayoutForWebPage(webPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
//
// for (_, tileNode) in self.visibleTiles {
// tileNode.removeFromSupernode()
// }
// self.visibleTiles.removeAll()
//
// let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width)
//
// var currentDetailsItems: [InstantPageDetailsItem] = []
// var currentLayoutItemsWithNodes: [InstantPageItem] = []
// var distanceThresholdGroupCount: [Int : Int] = [:]
//
// var expandedDetails: [Int : Bool] = [:]
//
// var detailsIndex = -1
// for item in currentLayout.items {
// if item.wantsNode {
// currentLayoutItemsWithNodes.append(item)
// if let group = item.distanceThresholdGroup() {
// let count: Int
// if let currentCount = distanceThresholdGroupCount[Int(group)] {
// count = currentCount
// } else {
// count = 0
// }
// distanceThresholdGroupCount[Int(group)] = count + 1
// }
// if let detailsItem = item as? InstantPageDetailsItem {
// detailsIndex += 1
// expandedDetails[detailsIndex] = detailsItem.initiallyExpanded
// currentDetailsItems.append(detailsItem)
// }
// }
// }
//
// if var currentExpandedDetails = self.currentExpandedDetails {
// for (index, expanded) in expandedDetails {
// if currentExpandedDetails[index] == nil {
// currentExpandedDetails[index] = expanded
// }
// }
// self.currentExpandedDetails = currentExpandedDetails
// } else {
// self.currentExpandedDetails = expandedDetails
// }
//
// let accessibilityAreas = instantPageAccessibilityAreasFromLayout(currentLayout, boundingWidth: containerLayout.size.width)
//
// self.currentLayout = currentLayout
// self.currentLayoutTiles = currentLayoutTiles
// self.currentLayoutItemsWithNodes = currentLayoutItemsWithNodes
// self.currentDetailsItems = currentDetailsItems
// self.distanceThresholdGroupCount = distanceThresholdGroupCount
//
// for areaNode in self.currentAccessibilityAreas {
// areaNode.removeFromSupernode()
// }
// for areaNode in accessibilityAreas {
// self.scrollNode.addSubnode(areaNode)
// }
// self.currentAccessibilityAreas = accessibilityAreas
//
// self.scrollNode.view.contentSize = currentLayout.contentSize
// self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: currentLayout.contentSize.height), size: CGSize(width: containerLayout.size.width, height: 2000.0))
// }
//
// func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) {
// guard let theme = self.theme else {
// return
// }
//
// var visibleTileIndices = Set<Int>()
// var visibleItemIndices = Set<Int>()
//
// var topNode: ASDisplayNode?
// let topTileNode = topNode
// if let scrollSubnodes = self.scrollNode.subnodes {
// for node in scrollSubnodes.reversed() {
// if let node = node as? InstantPageTileNode {
// topNode = node
// break
// }
// }
// }
//
// var collapseOffset: CGFloat = 0.0
// let transition: ContainedViewLayoutTransition
// if animated {
// transition = .animated(duration: 0.3, curve: .spring)
// } else {
// transition = .immediate
// }
//
// var itemIndex = -1
// var embedIndex = -1
// var detailsIndex = -1
//
// var previousDetailsNode: InstantPageDetailsNode?
//
// for item in self.currentLayoutItemsWithNodes {
// itemIndex += 1
// if item is InstantPageWebEmbedItem {
// embedIndex += 1
// }
// if let imageItem = item as? InstantPageImageItem, imageItem.media.media is TelegramMediaWebpage {
// embedIndex += 1
// }
// if item is InstantPageDetailsItem {
// detailsIndex += 1
// }
//
// var itemThreshold: CGFloat = 0.0
// if let group = item.distanceThresholdGroup() {
// var count: Int = 0
// if let currentCount = self.distanceThresholdGroupCount[group] {
// count = currentCount
// }
// itemThreshold = item.distanceThresholdWithGroupCount(count)
// }
//
// var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset)
// var thresholdedItemFrame = itemFrame
// thresholdedItemFrame.origin.y -= itemThreshold
// thresholdedItemFrame.size.height += itemThreshold * 2.0
//
// if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] {
// let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight
// collapseOffset += itemFrame.height - height
// itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height))
// }
//
// if visibleBounds.intersects(thresholdedItemFrame) {
// visibleItemIndices.insert(itemIndex)
//
// var itemNode = self.visibleItemsWithNodes[itemIndex]
// if let currentItemNode = itemNode {
// if !item.matchesNode(currentItemNode) {
// currentItemNode.removeFromSupernode()
// self.visibleItemsWithNodes.removeValue(forKey: itemIndex)
// itemNode = nil
// }
// }
//
// if itemNode == nil {
// let itemIndex = itemIndex
// let embedIndex = embedIndex
// let detailsIndex = detailsIndex
// if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourceLocation: self.sourceLocation, openMedia: { [weak self] media in
// self?.openMedia(media)
// }, longPressMedia: { [weak self] media in
// self?.longPressMedia(media)
// }, activatePinchPreview: { [weak self] sourceNode in
// guard let strongSelf = self, let controller = strongSelf.controller else {
// return
// }
// let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
// guard let strongSelf = self else {
// return CGRect()
// }
//
// let localRect = CGRect(origin: CGPoint(x: 0.0, y: strongSelf.navigationBar.frame.maxY), size: CGSize(width: strongSelf.bounds.width, height: strongSelf.bounds.height - strongSelf.navigationBar.frame.maxY))
// return strongSelf.view.convert(localRect, to: nil)
// })
// controller.window?.presentInGlobalOverlay(pinchController)
// }, pinchPreviewFinished: { [weak self] itemNode in
// guard let strongSelf = self else {
// return
// }
// for (_, listItemNode) in strongSelf.visibleItemsWithNodes {
// if let listItemNode = listItemNode as? InstantPagePeerReferenceNode {
// if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 {
// listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25)
// break
// }
// }
// }
// }, openPeer: { [weak self] peerId in
// self?.openPeer(peerId)
// }, openUrl: { [weak self] url in
// self?.openUrl(url)
// }, updateWebEmbedHeight: { [weak self] height in
// self?.updateWebEmbedHeight(embedIndex, height)
// }, updateDetailsExpanded: { [weak self] expanded in
// self?.updateDetailsExpanded(detailsIndex, expanded)
// }, currentExpandedDetails: self.currentExpandedDetails) {
// newNode.frame = itemFrame
// newNode.updateLayout(size: itemFrame.size, transition: transition)
// if let topNode = topNode {
// self.scrollNode.insertSubnode(newNode, aboveSubnode: topNode)
// } else {
// self.scrollNode.insertSubnode(newNode, at: 0)
// }
// topNode = newNode
// self.visibleItemsWithNodes[itemIndex] = newNode
// itemNode = newNode
//
// if let itemNode = itemNode as? InstantPageDetailsNode {
// itemNode.requestLayoutUpdate = { [weak self] animated in
// if let strongSelf = self {
// strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds, animated: animated)
// }
// }
//
// if let previousDetailsNode = previousDetailsNode {
// if itemNode.frame.minY - previousDetailsNode.frame.maxY < 1.0 {
// itemNode.previousNode = previousDetailsNode
// }
// }
// previousDetailsNode = itemNode
// }
// }
// } else {
// if let itemNode = itemNode, itemNode.frame != itemFrame {
// transition.updateFrame(node: itemNode, frame: itemFrame)
// itemNode.updateLayout(size: itemFrame.size, transition: transition)
// }
// }
//
// if let itemNode = itemNode as? InstantPageDetailsNode {
// itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
// }
// }
// }
//
// topNode = topTileNode
//
// var tileIndex = -1
// for tile in self.currentLayoutTiles {
// tileIndex += 1
//
// let tileFrame = effectiveFrameForTile(tile)
// var tileVisibleFrame = tileFrame
// tileVisibleFrame.origin.y -= 400.0
// tileVisibleFrame.size.height += 400.0 * 2.0
// if tileVisibleFrame.intersects(visibleBounds) {
// visibleTileIndices.insert(tileIndex)
//
// if self.visibleTiles[tileIndex] == nil {
// let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor)
// tileNode.frame = tileFrame
// if let topNode = topNode {
// self.scrollNode.insertSubnode(tileNode, aboveSubnode: topNode)
// } else {
// self.scrollNode.insertSubnode(tileNode, at: 0)
// }
// topNode = tileNode
// self.visibleTiles[tileIndex] = tileNode
// } else {
// if visibleTiles[tileIndex]!.frame != tileFrame {
// transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
// }
// }
// }
// }
//
// if let currentLayout = self.currentLayout {
// let effectiveContentHeight = currentLayout.contentSize.height - collapseOffset
// if effectiveContentHeight != self.scrollNode.view.contentSize.height {
// transition.animateView {
// self.scrollNode.view.contentSize = CGSize(width: currentLayout.contentSize.width, height: effectiveContentHeight)
// }
// let previousFrame = self.scrollNodeFooter.frame
// self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: effectiveContentHeight), size: CGSize(width: previousFrame.width, height: 2000.0))
// transition.animateFrame(node: self.scrollNodeFooter, from: previousFrame)
// }
// }
//
// var removeTileIndices: [Int] = []
// for (index, tileNode) in self.visibleTiles {
// if !visibleTileIndices.contains(index) {
// removeTileIndices.append(index)
// tileNode.removeFromSupernode()
// }
// }
// for index in removeTileIndices {
// self.visibleTiles.removeValue(forKey: index)
// }
//
// var removeItemIndices: [Int] = []
// for (index, itemNode) in self.visibleItemsWithNodes {
// if !visibleItemIndices.contains(index) {
// removeItemIndices.append(index)
// itemNode.removeFromSupernode()
// } else {
// var itemFrame = itemNode.frame
// let itemThreshold: CGFloat = 200.0
// itemFrame.origin.y -= itemThreshold
// itemFrame.size.height += itemThreshold * 2.0
// itemNode.updateIsVisible(visibleBounds.intersects(itemFrame))
// }
// }
// for index in removeItemIndices {
// self.visibleItemsWithNodes.removeValue(forKey: index)
// }
// }
//
// func scrollViewDidScroll(_ scrollView: UIScrollView) {
// self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
// self.previousContentOffset = self.scrollNode.view.contentOffset
// }
//
// func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// self.isDeceleratingBecauseOfDragging = decelerate
// if !decelerate {
// self.updateNavigationBar(forceState: true)
// }
// }
//
// func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// self.isDeceleratingBecauseOfDragging = false
// }
//
// private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint {
// var contentOffset = CGPoint()
// for (_, itemNode) in self.visibleItemsWithNodes {
// if let itemNode = itemNode as? InstantPageScrollableNode, itemNode.item === item {
// contentOffset = itemNode.contentOffset
// break
// }
// }
// return contentOffset
// }
//
// private func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? {
// for (_, itemNode) in self.visibleItemsWithNodes {
// if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item {
// return detailsNode
// }
// }
// return nil
// }
//
// private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize {
// if let node = nodeForDetailsItem(item) {
// return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight)
// } else {
// return item.frame.size
// }
// }
//
// private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect {
// let layoutOrigin = tile.frame.origin
// var origin = layoutOrigin
// for item in self.currentDetailsItems {
// let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
// if layoutOrigin.y >= item.frame.maxY {
// let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
// origin.y += height - item.frame.height
// }
// }
// return CGRect(origin: origin, size: tile.frame.size)
// }
//
// private func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect {
// let layoutOrigin = item.frame.origin
// var origin = layoutOrigin
//
// for item in self.currentDetailsItems {
// let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
// if layoutOrigin.y >= item.frame.maxY {
// let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
// origin.y += height - item.frame.height
// }
// }
//
// if let item = item as? InstantPageDetailsItem {
// let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
// let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
// return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height))
// } else {
// return CGRect(origin: origin, size: item.frame.size)
// }
// }
//
// private func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
// if let currentLayout = self.currentLayout {
// for item in currentLayout.items {
// let itemFrame = self.effectiveFrameForItem(item)
// if itemFrame.contains(location) {
// if let item = item as? InstantPageTextItem, item.selectable {
// return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY))
// } else if let item = item as? InstantPageScrollableItem {
// let contentOffset = scrollableContentOffset(item: item)
// if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) {
// return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y))
// }
// } else if let item = item as? InstantPageDetailsItem {
// for (_, itemNode) in self.visibleItemsWithNodes {
// if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
// if let (textItem, parentOffset) = itemNode.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) {
// return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y))
// }
// }
// }
// }
// }
// }
// }
// return nil
// }
//
// private func urlForTapLocation(_ location: CGPoint) -> InstantPageUrlItem? {
// if let (item, parentOffset) = self.textItemAtLocation(location) {
// return item.urlAttribute(at: location.offsetBy(dx: -item.frame.minX - parentOffset.x, dy: -item.frame.minY - parentOffset.y))
// }
// return nil
// }
//
// private func longPressMedia(_ media: InstantPageMedia) {
// let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
// if let strongSelf = self, let image = media.media as? TelegramMediaImage {
// let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
// let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
// }
// }), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in
// if let strongSelf = self, let image = media.media as? TelegramMediaImage {
// let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
// let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
// }
// }), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
// if let strongSelf = self, let webPage = strongSelf.webPage, let image = media.media as? TelegramMediaImage {
// strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil)
// }
// })], catchTapsOutside: true)
// self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
// if let strongSelf = self {
// for (_, itemNode) in strongSelf.visibleItemsWithNodes {
// if let (node, _, _) = itemNode.transitionNode(media: media) {
// return (strongSelf.scrollNode, node.convert(node.bounds, to: strongSelf.scrollNode), strongSelf, strongSelf.bounds)
// }
// }
// }
// return nil
// }))
// }
//
// @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
// switch recognizer.state {
// case .ended:
// if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
// switch gesture {
// case .tap:
// break
//// if let url = self.urlForTapLocation(location) {
//// self.openUrl(url)
//// }
// case .longTap:
// break
//// if let theme = self.theme, let url = self.urlForTapLocation(location) {
//// let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url.url)).count > 1
//// let openText = canOpenIn ? self.strings.Conversation_FileOpenIn : self.strings.Conversation_LinkDialogOpen
//// let actionSheet = ActionSheetController(instantPageTheme: theme)
//// actionSheet.setItemGroups([ActionSheetItemGroup(items: [
//// ActionSheetTextItem(title: url.url),
//// ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in
//// actionSheet?.dismissAnimated()
//// if let strongSelf = self {
//// if canOpenIn {
//// strongSelf.openUrlIn(url)
//// } else {
//// strongSelf.openUrl(url)
//// }
//// }
//// }),
//// ActionSheetButtonItem(title: self.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in
//// actionSheet?.dismissAnimated()
//// UIPasteboard.general.string = url.url
//// }),
//// ActionSheetButtonItem(title: self.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
//// actionSheet?.dismissAnimated()
//// if let link = URL(string: url.url) {
//// let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
//// }
//// })
//// ]), ActionSheetItemGroup(items: [
//// ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
//// actionSheet?.dismissAnimated()
//// })
//// ])])
//// self.present(actionSheet, nil)
//// } else if let (item, parentOffset) = self.textItemAtLocation(location) {
//// let textFrame = item.frame
//// var itemRects = item.lineRects()
//// for i in 0 ..< itemRects.count {
//// itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textFrame.minX, dy: parentOffset.y + textFrame.minY).insetBy(dx: -2.0, dy: -2.0)
//// }
//// self.updateTextSelectionRects(itemRects, text: item.plainText())
//// }
// default:
// break
// }
// }
// default:
// break
// }
// }
//
// private func updateTextSelectionRects(_ rects: [CGRect], text: String?) {
// if let text = text, !rects.isEmpty {
// let textSelectionNode: LinkHighlightingNode
// if let current = self.textSelectionNode {
// textSelectionNode = current
// } else {
// textSelectionNode = LinkHighlightingNode(color: UIColor.lightGray.withAlphaComponent(0.4))
// textSelectionNode.isUserInteractionEnabled = false
// self.textSelectionNode = textSelectionNode
// self.scrollNode.addSubnode(textSelectionNode)
// }
// textSelectionNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
// textSelectionNode.updateRects(rects)
//
// var coveringRect = rects[0]
// for i in 1 ..< rects.count {
// coveringRect = coveringRect.union(rects[i])
// }
//
// let context = self.context
// let strings = self.strings
// let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
// |> take(1)
// |> deliverOnMainQueue).start(next: { [weak self] sharedData in
// let translationSettings: TranslationSettings
// if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
// translationSettings = current
// } else {
// translationSettings = TranslationSettings.defaultSettings
// }
//
// var actions: [ContextMenuAction] = [ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuCopy, accessibilityLabel: strings.Conversation_ContextMenuCopy), action: { [weak self] in
// UIPasteboard.general.string = text
//
// if let strongSelf = self {
// let presentationData = context.sharedContext.currentPresentationData.with { $0 }
// strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
// }
// }), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in
// if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content {
// strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil)
// }
// })]
//
// let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages)
// if canTranslate {
// actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { [weak self] in
// let controller = TranslateScreen(context: context, text: text, canCopy: true, fromLanguage: language)
// controller.pushController = { [weak self] c in
// (self?.controller?.navigationController as? NavigationController)?._keepModalDismissProgress = true
// self?.controller?.push(c)
// }
// controller.presentController = { [weak self] c in
// self?.controller?.present(c, in: .window(.root))
// }
// self?.present(controller, nil)
// }))
// }
//
// let controller = ContextMenuController(actions: actions)
// controller.dismissed = { [weak self] in
// self?.updateTextSelectionRects([], text: nil)
// }
// self?.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
// if let strongSelf = self {
// return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0), strongSelf, strongSelf.bounds)
// } else {
// return nil
// }
// }))
// })
//
// textSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
// } else if let textSelectionNode = self.textSelectionNode {
// self.textSelectionNode = nil
// textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
// textSelectionNode?.removeFromSupernode()
// })
// }
// }
//
// private func findAnchorItem(_ anchor: String, items: [InstantPageItem]) -> (InstantPageItem, CGFloat, Bool, [InstantPageDetailsItem])? {
// for item in items {
// if let item = item as? InstantPageAnchorItem, item.anchor == anchor {
// return (item, -10.0, false, [])
// } else if let item = item as? InstantPageTextItem {
// if let (lineIndex, empty) = item.anchors[anchor] {
// return (item, item.lines[lineIndex].frame.minY - 10.0, !empty, [])
// }
// }
// else if let item = item as? InstantPageTableItem {
// if let (offset, empty) = item.anchors[anchor] {
// return (item, offset - 10.0, !empty, [])
// }
// }
// else if let item = item as? InstantPageDetailsItem {
// if let (foundItem, offset, reference, detailsItems) = self.findAnchorItem(anchor, items: item.items) {
// var detailsItems = detailsItems
// detailsItems.insert(item, at: 0)
// return (foundItem, offset, reference, detailsItems)
// }
// }
// }
// return nil
// }
//
// private func presentReferenceView(item: InstantPageTextItem, referenceAnchor: String) {
//// guard let theme = self.theme, let webPage = self.webPage else {
//// return
//// }
////
//// var targetAnchor: InstantPageTextAnchorItem?
//// for (name, (line, _)) in item.anchors {
//// if name == referenceAnchor {
//// let anchors = item.lines[line].anchorItems
//// for anchor in anchors {
//// if anchor.name == referenceAnchor {
//// targetAnchor = anchor
//// break
//// }
//// }
//// }
//// }
////
//// guard let anchorText = targetAnchor?.anchorText else {
//// return
//// }
////
//// let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in
//// self?.openUrl(url)
//// }, openUrlIn: { [weak self] url in
//// self?.openUrlIn(url)
//// }, present: { [weak self] c, a in
//// self?.present(c, a)
//// })
//// self.present(controller, nil)
// }
//
// private func scrollToAnchor(_ anchor: String) {
// guard let items = self.currentLayout?.items else {
// return
// }
//
// if !anchor.isEmpty {
// if let (item, lineOffset, reference, detailsItems) = findAnchorItem(String(anchor), items: items) {
// if let item = item as? InstantPageTextItem, reference {
// self.presentReferenceView(item: item, referenceAnchor: anchor)
// } else {
// var previousDetailsNode: InstantPageDetailsNode?
// var containerOffset: CGFloat = 0.0
// for detailsItem in detailsItems {
// if let previousNode = previousDetailsNode {
// previousNode.contentNode.updateDetailsExpanded(detailsItem.index, true, animated: false)
// let frame = previousNode.effectiveFrameForItem(detailsItem)
// containerOffset += frame.minY
//
// previousDetailsNode = previousNode.contentNode.nodeForDetailsItem(detailsItem)
// previousDetailsNode?.setExpanded(true, animated: false)
// } else {
// self.updateDetailsExpanded(detailsItem.index, true, animated: false)
// let frame = self.effectiveFrameForItem(detailsItem)
// containerOffset += frame.minY
//
// previousDetailsNode = self.nodeForDetailsItem(detailsItem)
// previousDetailsNode?.setExpanded(true, animated: false)
// }
// }
//
// let frame: CGRect
// if let previousDetailsNode = previousDetailsNode {
// frame = previousDetailsNode.effectiveFrameForItem(item)
// } else {
// frame = self.effectiveFrameForItem(item)
// }
//
// var targetY = min(containerOffset + frame.minY + lineOffset, self.scrollNode.view.contentSize.height - self.scrollNode.frame.height)
// if targetY < self.scrollNode.view.contentOffset.y {
// targetY -= self.scrollNode.view.contentInset.top
// } else {
// targetY -= self.containerLayout?.statusBarHeight ?? 20.0
// }
// self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true)
// }
// } else if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, !instantPage.isComplete {
// self.loadProgress.set(0.5)
// self.pendingAnchor = anchor
// }
// } else {
// self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top), animated: true)
// }
// }
//
// private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
// let currentHeight = self.currentWebEmbedHeights[index]
// if height != currentHeight {
// if let currentHeight = currentHeight, currentHeight > height {
// return
// }
// self.currentWebEmbedHeights[index] = height
//
// let signal: Signal<Void, NoError> = (.complete() |> delay(0.08, queue: Queue.mainQueue()))
// self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in
// if let strongSelf = self {
// strongSelf.updateLayout()
// strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds)
// }
// }))
// }
// }
//
// private func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true) {
// if var currentExpandedDetails = self.currentExpandedDetails {
// currentExpandedDetails[index] = expanded
// self.currentExpandedDetails = currentExpandedDetails
// }
// self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated)
// }
//
//}
//
//final class BrowserInstantPageContent: UIView, BrowserContent {
// var onScrollingUpdate: (ContentScrollingUpdate) -> Void
//
// func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentFlow.Transition) {
//
// }
// //
// private var _state: BrowserContentState // private var _state: BrowserContentState
// private let statePromise: Promise<BrowserContentState> // private let statePromise: Promise<BrowserContentState>
@ -28,7 +891,6 @@
// self.webPage = webPage // self.webPage = webPage
// //
// let presentationData = context.sharedContext.currentPresentationData.with { $0 } // let presentationData = context.sharedContext.currentPresentationData.with { $0 }
// self.instantPageNode = InstantPageContentNode(context: context, webPage: webPage, settings: nil, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, sourcePeerType: .contact, getNavigationController: { return nil }, present: { _, _ in }, pushController: { _ in }, openPeer: { _ in }, navigateBack: {})
// //
// let title: String // let title: String
// if case let .Loaded(content) = webPage.content { // if case let .Loaded(content) = webPage.content {
@ -37,12 +899,16 @@
// title = "" // title = ""
// } // }
// //
// self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, isInstant: false) // self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .instantPage)
// self.statePromise = Promise<BrowserContentState>(self._state) // self.statePromise = Promise<BrowserContentState>(self._state)
// //
// super.init() // super.init()
// //
// self.addSubnode(self.instantPageNode) //
// }
//
// required init?(coder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
// } // }
// //
// func navigateBack() { // func navigateBack() {
@ -78,14 +944,14 @@
// } // }
// //
// func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { // func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
// let layout = ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: insets.bottom, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false) //// let layout = ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: insets.bottom, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)
// self.instantPageNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) //// self.instantPageNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
// self.instantPageNode.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height) //// self.instantPageNode.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
// //transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0))) //// //transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0)))
// ////
// if !self.initialized { //// if !self.initialized {
// self.initialized = true //// self.initialized = true
// self.instantPageNode.updateWebPage(self.webPage, anchor: nil) //// self.instantPageNode.updateWebPage(self.webPage, anchor: nil)
// } //// }
// } // }
//} //}

View File

@ -1,35 +0,0 @@
import Foundation
final class BrowserInteraction {
let navigateBack: () -> Void
let navigateForward: () -> Void
let share: () -> Void
let minimize: () -> Void
let openSearch: () -> Void
let updateSearchQuery: (String) -> Void
let dismissSearch: () -> Void
let scrollToPreviousSearchResult: () -> Void
let scrollToNextSearchResult: () -> Void
let decreaseFontSize: () -> Void
let increaseFontSize: () -> Void
let resetFontSize: () -> Void
let updateForceSerif: (Bool) -> Void
init(navigateBack: @escaping () -> Void, navigateForward: @escaping () -> Void, share: @escaping () -> Void, minimize: @escaping () -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, scrollToPreviousSearchResult: @escaping () -> Void, scrollToNextSearchResult: @escaping () -> Void, decreaseFontSize: @escaping () -> Void, increaseFontSize: @escaping () -> Void, resetFontSize: @escaping () -> Void, updateForceSerif: @escaping (Bool) -> Void) {
self.navigateBack = navigateBack
self.navigateForward = navigateForward
self.share = share
self.minimize = minimize
self.openSearch = openSearch
self.updateSearchQuery = updateSearchQuery
self.dismissSearch = dismissSearch
self.scrollToPreviousSearchResult = scrollToPreviousSearchResult
self.scrollToNextSearchResult = scrollToNextSearchResult
self.decreaseFontSize = decreaseFontSize
self.increaseFontSize = increaseFontSize
self.resetFontSize = resetFontSize
self.updateForceSerif = updateForceSerif
}
}

View File

@ -1,416 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import AppBundle
import ContextUI
private let closeImage = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: .black)
private let settingsImage = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings"), color: .black)
private func navigationBarContentNode(for state: BrowserState, currentContentNode: BrowserNavigationBarContentNode?, layoutMetrics: LayoutMetrics, theme: BrowserNavigationBarTheme, strings: PresentationStrings, interaction: BrowserInteraction?) -> BrowserNavigationBarContentNode? {
if let _ = state.search {
if let currentContentNode = currentContentNode as? BrowserNavigationBarSearchContentNode {
currentContentNode.updateState(state)
return currentContentNode
} else {
return BrowserNavigationBarSearchContentNode(theme: theme, strings: strings, state: state, interaction: interaction)
}
}
return nil
}
final class BrowserNavigationBarTheme {
let backgroundColor: UIColor
let separatorColor: UIColor
let primaryTextColor: UIColor
let loadingProgressColor: UIColor
let readingProgressColor: UIColor
let buttonColor: UIColor
let disabledButtonColor: UIColor
let searchBarFieldColor: UIColor
let searchBarTextColor: UIColor
let searchBarPlaceholderColor: UIColor
let searchBarIconColor: UIColor
let searchBarClearColor: UIColor
let searchBarKeyboardColor: PresentationThemeKeyboardColor
init(backgroundColor: UIColor, separatorColor: UIColor, primaryTextColor: UIColor, loadingProgressColor: UIColor, readingProgressColor: UIColor, buttonColor: UIColor, disabledButtonColor: UIColor, searchBarFieldColor: UIColor, searchBarTextColor: UIColor, searchBarPlaceholderColor: UIColor, searchBarIconColor: UIColor, searchBarClearColor: UIColor, searchBarKeyboardColor: PresentationThemeKeyboardColor) {
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
self.primaryTextColor = primaryTextColor
self.loadingProgressColor = loadingProgressColor
self.readingProgressColor = readingProgressColor
self.buttonColor = buttonColor
self.disabledButtonColor = disabledButtonColor
self.searchBarFieldColor = searchBarFieldColor
self.searchBarTextColor = searchBarTextColor
self.searchBarPlaceholderColor = searchBarPlaceholderColor
self.searchBarIconColor = searchBarIconColor
self.searchBarClearColor = searchBarClearColor
self.searchBarKeyboardColor = searchBarKeyboardColor
}
}
protocol BrowserNavigationBarContentNode: ASDisplayNode {
init(theme: BrowserNavigationBarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?)
func updateState(_ state: BrowserState)
func updateTheme(_ theme: BrowserNavigationBarTheme)
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
}
private final class BrowserLoadingProgressNode: ASDisplayNode {
private var theme: BrowserNavigationBarTheme
private let foregroundNode: ASDisplayNode
init(theme: BrowserNavigationBarTheme) {
self.theme = theme
self.foregroundNode = ASDisplayNode()
self.foregroundNode.backgroundColor = theme.loadingProgressColor
super.init()
self.addSubnode(self.foregroundNode)
}
func updateTheme(_ theme: BrowserNavigationBarTheme) {
self.theme = theme
self.foregroundNode.backgroundColor = theme.loadingProgressColor
}
private var _progress: CGFloat = 0.0
func updateProgress(_ progress: CGFloat, animated: Bool = false) {
if self._progress == progress && animated {
return
}
var animated = animated
if (progress < self._progress && animated) {
animated = false
}
let size = self.bounds.size
self._progress = progress
let transition: ContainedViewLayoutTransition
if animated && progress > 0.0 {
transition = .animated(duration: 0.7, curve: .spring)
} else {
transition = .immediate
}
let alpaTransition: ContainedViewLayoutTransition
if animated {
alpaTransition = .animated(duration: 0.3, curve: .easeInOut)
} else {
alpaTransition = .immediate
}
transition.updateFrame(node: self.foregroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width * progress, height: size.height))
let alpha: CGFloat = progress < 0.001 || progress > 0.999 ? 0.0 : 1.0
alpaTransition.updateAlpha(node: self.foregroundNode, alpha: alpha)
}
}
var browserNavigationBarHeight: CGFloat = 56.0
var browserNavigationBarCollapsedHeight: CGFloat = 24.0
final class BrowserNavigationBar: ASDisplayNode {
private var theme: BrowserNavigationBarTheme
private var strings: PresentationStrings
private var state: BrowserState
var interaction: BrowserInteraction?
private let containerNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let readingProgressNode: ASDisplayNode
private let loadingProgressNode: BrowserLoadingProgressNode
private let closeButton: HighlightableButtonNode
private let closeIconNode: ASImageNode
private let closeIconSmallNode: ASImageNode
let contextSourceNode: ContextExtractedContentContainingNode
private let backButton: HighlightableButtonNode
private let forwardButton: HighlightableButtonNode
private let shareButton: HighlightableButtonNode
private let minimizeButton: HighlightableButtonNode
private let settingsButton: HighlightableButtonNode
private let titleNode: ImmediateTextNode
private let scrollToTopButton: HighlightableButtonNode
private var contentNode: BrowserNavigationBarContentNode?
private let intrinsicSettingsSize: CGSize
private let intrinsicSmallSettingsSize: CGSize
private var validLayout: (CGSize, UIEdgeInsets, LayoutMetrics, CGFloat, CGFloat)?
private var title: (String, Bool) = ("", false) {
didSet {
self.updateTitle()
}
}
private func updateTitle() {
if let (size, insets, layoutMetrics, readingProgress, collapseTransition) = self.validLayout {
self.titleNode.attributedText = NSAttributedString(string: self.title.0, font: Font.with(size: 17.0, design: self.title.1 ? .serif : .regular, weight: .bold), textColor: self.theme.primaryTextColor, paragraphAlignment: .center)
let sideInset: CGFloat = 56.0
self.titleNode.transform = CATransform3DIdentity
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - insets.left - insets.right - sideInset * 2.0, height: size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: (size.width - titleSize.width) / 2.0, y: size.height - 30.0), size: titleSize)
self.updateLayout(size: size, insets: insets, layoutMetrics: layoutMetrics, readingProgress: readingProgress, collapseTransition: collapseTransition, transition: .immediate)
}
}
var close: (() -> Void)?
var openSettings: (() -> Void)?
var scrollToTop: (() -> Void)?
init(theme: BrowserNavigationBarTheme, strings: PresentationStrings, state: BrowserState) {
self.theme = theme
self.strings = strings
self.state = state
self.containerNode = ASDisplayNode()
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.separatorColor
self.readingProgressNode = ASDisplayNode()
self.readingProgressNode.isLayerBacked = true
self.readingProgressNode.backgroundColor = theme.readingProgressColor
self.closeButton = HighlightableButtonNode()
self.closeButton.allowsGroupOpacity = true
self.closeIconNode = ASImageNode()
self.closeIconNode.displaysAsynchronously = false
self.closeIconNode.displayWithoutProcessing = true
self.closeIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: theme.buttonColor)
self.closeIconSmallNode = ASImageNode()
self.closeIconSmallNode.displaysAsynchronously = false
self.closeIconSmallNode.displayWithoutProcessing = true
self.closeIconSmallNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/CloseSmall"), color: theme.buttonColor)
self.closeIconSmallNode.alpha = 0.0
self.contextSourceNode = ContextExtractedContentContainingNode()
self.settingsButton = HighlightableButtonNode()
self.settingsButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings"), color: theme.buttonColor), for: [])
self.intrinsicSettingsSize = CGSize(width: browserNavigationBarHeight, height: browserNavigationBarHeight)
self.intrinsicSmallSettingsSize = CGSize(width: browserNavigationBarCollapsedHeight, height: browserNavigationBarCollapsedHeight)
self.settingsButton.frame = CGRect(origin: CGPoint(), size: self.intrinsicSettingsSize)
self.backButton = HighlightableButtonNode()
self.backButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Back"), color: theme.buttonColor), for: [])
self.backButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Back"), color: theme.disabledButtonColor), for: [.disabled])
self.forwardButton = HighlightableButtonNode()
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Forward"), color: theme.buttonColor), for: [])
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Forward"), color: theme.disabledButtonColor), for: [.disabled])
self.shareButton = HighlightableButtonNode()
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.buttonColor), for: [])
self.minimizeButton = HighlightableButtonNode()
self.minimizeButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Minimize"), color: theme.buttonColor), for: [])
self.titleNode = ImmediateTextNode()
self.titleNode.textAlignment = .center
self.titleNode.maximumNumberOfLines = 1
self.scrollToTopButton = HighlightableButtonNode()
self.loadingProgressNode = BrowserLoadingProgressNode(theme: theme)
super.init()
self.clipsToBounds = true
self.containerNode.backgroundColor = theme.backgroundColor
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.readingProgressNode)
self.containerNode.addSubnode(self.closeButton)
self.closeButton.addSubnode(self.closeIconNode)
self.closeButton.addSubnode(self.closeIconSmallNode)
self.containerNode.addSubnode(self.contextSourceNode)
self.contextSourceNode.addSubnode(self.settingsButton)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.scrollToTopButton)
self.containerNode.addSubnode(self.loadingProgressNode)
self.containerNode.addSubnode(self.separatorNode)
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside)
self.settingsButton.addTarget(self, action: #selector(self.settingsPressed), forControlEvents: .touchUpInside)
self.scrollToTopButton.addTarget(self, action: #selector(self.scrollToTopPressed), forControlEvents: .touchUpInside)
self.title = (state.content?.title ?? "", state.content?.isInstant ?? false)
}
func updateState(_ state: BrowserState) {
self.state = state
if let (size, insets, layoutMetrics, readingProgress, collapseTransition) = self.validLayout {
self.updateLayout(size: size, insets: insets, layoutMetrics: layoutMetrics, readingProgress: readingProgress, collapseTransition: collapseTransition, transition: .animated(duration: 0.2, curve: .easeInOut))
}
self.title = (state.content?.title ?? "", state.content?.isInstant ?? false)
self.loadingProgressNode.updateProgress(CGFloat(state.content?.estimatedProgress ?? 0.0), animated: true)
}
func updateTheme(_ theme: BrowserNavigationBarTheme) {
guard self.theme !== theme else {
return
}
self.theme = theme
self.containerNode.backgroundColor = theme.backgroundColor
self.separatorNode.backgroundColor = theme.separatorColor
self.closeIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: theme.buttonColor)
self.closeIconSmallNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/CloseSmall"), color: theme.buttonColor)
self.settingsButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings"), color: theme.buttonColor), for: [])
self.readingProgressNode.backgroundColor = theme.readingProgressColor
self.loadingProgressNode.updateTheme(theme)
self.updateTitle()
}
@objc private func closePressed() {
self.close?()
}
@objc private func settingsPressed() {
self.openSettings?()
}
@objc private func scrollToTopPressed() {
self.scrollToTop?()
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, layoutMetrics: LayoutMetrics, readingProgress: CGFloat, collapseTransition: CGFloat, transition: ContainedViewLayoutTransition) {
let hadValidLayout = self.validLayout != nil
self.validLayout = (size, insets, layoutMetrics, readingProgress, collapseTransition)
var dismissedContentNode: ASDisplayNode?
var immediatelyLayoutContentNodeAndAnimateAppearance = false
if let contentNode = navigationBarContentNode(for: self.state, currentContentNode: self.contentNode, layoutMetrics: layoutMetrics, theme: self.theme, strings: self.strings, interaction: self.interaction) {
if contentNode !== self.contentNode {
dismissedContentNode = self.contentNode
immediatelyLayoutContentNodeAndAnimateAppearance = true
self.containerNode.insertSubnode(contentNode, belowSubnode: self.separatorNode)
self.contentNode = contentNode
}
} else {
dismissedContentNode = self.contentNode
self.contentNode = nil
}
let expandTransition = 1.0 - collapseTransition
let maxBarHeight: CGFloat
let minBarHeight: CGFloat
if insets.top.isZero {
maxBarHeight = browserNavigationBarHeight
minBarHeight = browserNavigationBarCollapsedHeight
} else {
maxBarHeight = insets.top + 44.0
minBarHeight = insets.top + browserNavigationBarCollapsedHeight
}
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -(maxBarHeight - minBarHeight) * collapseTransition), size: size)
transition.updateFrame(node: self.containerNode, frame: containerFrame)
transition.updateFrame(node: self.readingProgressNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: floorToScreenPixels(size.width * readingProgress), height: size.height)))
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: size.height)))
if let image = self.closeIconNode.image {
let closeIconSize = image.size
let arrowHeight: CGFloat
if expandTransition < 1.0 {
arrowHeight = floor(12.0 * expandTransition + 18.0)
} else {
arrowHeight = 30.0
}
let scaledIconSize = CGSize(width: closeIconSize.width * arrowHeight / closeIconSize.height, height: arrowHeight)
let arrowOffset = floor(9.0 * expandTransition + 3.0)
transition.updateFrame(node: self.closeIconNode, frame: CGRect(origin: CGPoint(x: insets.left + 8.0, y: size.height - arrowHeight - arrowOffset), size: scaledIconSize))
}
let offsetScaleTransition: CGFloat
let buttonScaleTransition: CGFloat
if expandTransition < 1.0 {
offsetScaleTransition = expandTransition
buttonScaleTransition = ((expandTransition * self.intrinsicSettingsSize.height) + ((1.0 - expandTransition) * self.intrinsicSmallSettingsSize.height)) / self.intrinsicSettingsSize.height
} else {
offsetScaleTransition = 1.0
buttonScaleTransition = 1.0
}
let alphaTransition = min(1.0, offsetScaleTransition * offsetScaleTransition)
let maxSettingsOffset = floor(self.intrinsicSettingsSize.height / 2.0)
let minSettingsOffset = floor(self.intrinsicSmallSettingsSize.height / 2.0)
let settingsOffset = expandTransition * maxSettingsOffset + (1.0 - expandTransition) * minSettingsOffset
transition.updateTransformScale(node: self.titleNode, scale: 0.65 + expandTransition * 0.35)
transition.updatePosition(node: self.titleNode, position: CGPoint(x: size.width / 2.0, y: size.height - settingsOffset))
self.contextSourceNode.frame = CGRect(origin: CGPoint(x: size.width - 56.0, y: 0.0), size: CGSize(width: 56.0, height: 56.0))
transition.updateTransformScale(node: self.settingsButton, scale: buttonScaleTransition)
transition.updatePosition(node: self.settingsButton, position: CGPoint(x: 56.0 - insets.right - buttonScaleTransition * self.intrinsicSettingsSize.width / 2.0, y: size.height - settingsOffset))
transition.updateAlpha(node: self.settingsButton, alpha: alphaTransition)
transition.updateFrame(node: self.scrollToTopButton, frame: CGRect(origin: CGPoint(x: insets.left + 64.0, y: 0.0), size: CGSize(width: size.width - insets.left - insets.right - 64.0 * 2.0, height: size.height)))
let loadingProgressHeight: CGFloat = 2.0
transition.updateFrame(node: self.loadingProgressNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - loadingProgressHeight - UIScreenPixel), size: CGSize(width: size.width, height: loadingProgressHeight)))
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)))
let constrainedSize = CGSize(width: size.width, height: size.height)
if let contentNode = self.contentNode {
let contentNodeFrame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: constrainedSize)
contentNode.updateLayout(size: constrainedSize, transition: transition)
if immediatelyLayoutContentNodeAndAnimateAppearance {
contentNode.alpha = 0.0
}
transition.updateFrame(node: contentNode, frame: contentNodeFrame)
transition.updateAlpha(node: contentNode, alpha: 1.0)
}
if let dismissedContentNode = dismissedContentNode {
var alphaCompleted = false
let frameCompleted = true
let completed = { [weak self, weak dismissedContentNode] in
if let strongSelf = self, let dismissedContentNode = dismissedContentNode, strongSelf.contentNode === dismissedContentNode {
return
}
if frameCompleted && alphaCompleted {
dismissedContentNode?.removeFromSupernode()
}
}
transition.updateAlpha(node: dismissedContentNode, alpha: 0.0, completion: { _ in
alphaCompleted = true
completed()
})
}
if !hadValidLayout {
self.updateTitle()
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if let result = result, result.isDescendant(of: self.containerNode.view) || result == self.containerNode.view {
return result
}
return nil
}
}

View File

@ -0,0 +1,443 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import BlurredBackgroundComponent
import ContextUI
final class BrowserNavigationBarComponent: CombinedComponent {
let backgroundColor: UIColor
let separatorColor: UIColor
let textColor: UIColor
let progressColor: UIColor
let accentColor: UIColor
let topInset: CGFloat
let height: CGFloat
let sideInset: CGFloat
let leftItems: [AnyComponentWithIdentity<Empty>]
let rightItems: [AnyComponentWithIdentity<Empty>]
let centerItem: AnyComponentWithIdentity<Empty>?
let readingProgress: CGFloat
let loadingProgress: Double?
let collapseFraction: CGFloat
init(
backgroundColor: UIColor,
separatorColor: UIColor,
textColor: UIColor,
progressColor: UIColor,
accentColor: UIColor,
topInset: CGFloat,
height: CGFloat,
sideInset: CGFloat,
leftItems: [AnyComponentWithIdentity<Empty>],
rightItems: [AnyComponentWithIdentity<Empty>],
centerItem: AnyComponentWithIdentity<Empty>?,
readingProgress: CGFloat,
loadingProgress: Double?,
collapseFraction: CGFloat
) {
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
self.textColor = textColor
self.progressColor = progressColor
self.accentColor = accentColor
self.topInset = topInset
self.height = height
self.sideInset = sideInset
self.leftItems = leftItems
self.rightItems = rightItems
self.centerItem = centerItem
self.readingProgress = readingProgress
self.loadingProgress = loadingProgress
self.collapseFraction = collapseFraction
}
static func ==(lhs: BrowserNavigationBarComponent, rhs: BrowserNavigationBarComponent) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.progressColor != rhs.progressColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.topInset != rhs.topInset {
return false
}
if lhs.height != rhs.height {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.leftItems != rhs.leftItems {
return false
}
if lhs.rightItems != rhs.rightItems {
return false
}
if lhs.centerItem != rhs.centerItem {
return false
}
if lhs.readingProgress != rhs.readingProgress {
return false
}
if lhs.loadingProgress != rhs.loadingProgress {
return false
}
if lhs.collapseFraction != rhs.collapseFraction {
return false
}
return true
}
static var body: Body {
let background = Child(BlurredBackgroundComponent.self)
let readingProgress = Child(Rectangle.self)
let separator = Child(Rectangle.self)
let loadingProgress = Child(LoadingProgressComponent.self)
let leftItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let centerItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
return { context in
var availableWidth = context.availableSize.width
let sideInset: CGFloat = 11.0 + context.component.sideInset
let collapsedHeight: CGFloat = 24.0
let expandedHeight = context.component.height
let contentHeight: CGFloat = expandedHeight * (1.0 - context.component.collapseFraction) + collapsedHeight * context.component.collapseFraction
let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight)
let background = background.update(
component: BlurredBackgroundComponent(color: context.component.backgroundColor),
availableSize: CGSize(width: size.width, height: size.height),
transition: context.transition
)
let readingProgress = readingProgress.update(
component: Rectangle(color: context.component.progressColor),
availableSize: CGSize(width: size.width * context.component.readingProgress, height: size.height),
transition: context.transition
)
let separator = separator.update(
component: Rectangle(color: context.component.separatorColor, height: UIScreenPixel),
availableSize: CGSize(width: size.width, height: size.height),
transition: context.transition
)
let loadingProgressHeight: CGFloat = 2.0
let loadingProgress = loadingProgress.update(
component: LoadingProgressComponent(
color: context.component.accentColor,
height: loadingProgressHeight,
value: context.component.loadingProgress ?? 0.0
),
availableSize: CGSize(width: size.width, height: size.height),
transition: context.transition
)
var leftItemList: [_UpdatedChildComponent] = []
for item in context.component.leftItems {
let item = leftItems[item.id].update(
component: item.component,
availableSize: CGSize(width: availableWidth, height: expandedHeight),
transition: context.transition
)
leftItemList.append(item)
availableWidth -= item.size.width
}
var rightItemList: [_UpdatedChildComponent] = []
for item in context.component.rightItems {
let item = rightItems[item.id].update(
component: item.component,
availableSize: CGSize(width: availableWidth, height: expandedHeight),
transition: context.transition
)
rightItemList.append(item)
availableWidth -= item.size.width
}
let centerItem = context.component.centerItem.flatMap { item in
centerItems[item.id].update(
component: item.component,
availableSize: CGSize(width: availableWidth, height: expandedHeight),
transition: context.transition
)
}
if let centerItem = centerItem {
availableWidth -= centerItem.size.width
}
context.add(background
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
)
context.add(readingProgress
.position(CGPoint(x: readingProgress.size.width / 2.0, y: size.height / 2.0))
)
context.add(separator
.position(CGPoint(x: size.width / 2.0, y: size.height))
)
context.add(loadingProgress
.position(CGPoint(x: size.width / 2.0, y: size.height - loadingProgressHeight / 2.0))
)
var centerLeftInset = sideInset
var leftItemX = sideInset
for item in leftItemList {
context.add(item
.position(CGPoint(x: leftItemX + item.size.width / 2.0 - (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0))
.scale(1.0 - 0.35 * context.component.collapseFraction)
.appear(.default(scale: false, alpha: true))
.disappear(.default(scale: false, alpha: true))
)
leftItemX -= item.size.width + 8.0
centerLeftInset += item.size.width + 8.0
}
var centerRightInset = sideInset
var rightItemX = context.availableSize.width - sideInset
for item in rightItemList.reversed() {
context.add(item
.position(CGPoint(x: rightItemX - item.size.width / 2.0 + (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0))
.scale(1.0 - 0.35 * context.component.collapseFraction)
.opacity(1.0 - context.component.collapseFraction)
.appear(.default(scale: false, alpha: true))
.disappear(.default(scale: false, alpha: true))
)
rightItemX -= item.size.width + 8.0
centerRightInset += item.size.width + 8.0
}
let maxCenterInset = max(centerLeftInset, centerRightInset)
if let centerItem = centerItem {
context.add(centerItem
.position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset - maxCenterInset) / 2.0, y: context.component.topInset + contentHeight / 2.0))
.scale(1.0 - 0.35 * context.component.collapseFraction)
.appear(.default(scale: false, alpha: true))
.disappear(.default(scale: false, alpha: true))
)
}
return size
}
}
}
private final class LoadingProgressComponent: Component {
let color: UIColor
let height: CGFloat
let value: CGFloat
init(
color: UIColor,
height: CGFloat,
value: CGFloat
) {
self.color = color
self.height = height
self.value = value
}
static func ==(lhs: LoadingProgressComponent, rhs: LoadingProgressComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.height != rhs.height {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
final class View: UIView {
private var lineView: UIView
private var currentValue: Double = 0.0
init() {
self.lineView = UIView()
self.lineView.clipsToBounds = true
self.lineView.layer.cornerRadius = 1.0
self.lineView.alpha = 0.0
super.init(frame: CGRect())
self.addSubview(self.lineView)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: LoadingProgressComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.lineView.backgroundColor = component.color
let value = component.value
let frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width * component.value, height: component.height))
var animated = true
if value < self.currentValue {
if self.currentValue == 1.0 {
self.lineView.frame = CGRect(origin: .zero, size: CGSize(width: 0.0, height: component.height))
} else {
animated = false
}
}
self.currentValue = value
let transition: Transition
if animated && value > 0.0 {
transition = .spring(duration: 0.7)
} else {
transition = .immediate
}
let alphaTransition: Transition
if animated {
alphaTransition = .easeInOut(duration: 0.3)
} else {
alphaTransition = .immediate
}
transition.setFrame(view: self.lineView, frame: frame)
let alpha: CGFloat = value < 0.01 || value > 0.99 ? 0.0 : 1.0
alphaTransition.setAlpha(view: self.lineView, alpha: alpha)
return CGSize(width: availableSize.width, height: component.height)
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class ReferenceButtonComponent: Component {
let content: AnyComponent<Empty>
let tag: AnyObject?
let action: () -> Void
init(
content: AnyComponent<Empty>,
tag: AnyObject? = nil,
action: @escaping () -> Void
) {
self.content = content
self.tag = tag
self.action = action
}
static func ==(lhs: ReferenceButtonComponent, rhs: ReferenceButtonComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.tag !== rhs.tag {
return false
}
return true
}
final class View: HighlightTrackingButton, ComponentTaggedView {
private let sourceView: ContextControllerSourceView
let referenceNode: ContextReferenceContentNode
private let componentView: ComponentView<Empty>
private var component: ReferenceButtonComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
init() {
self.componentView = ComponentView()
self.sourceView = ContextControllerSourceView()
self.sourceView.animateScale = false
self.referenceNode = ContextReferenceContentNode()
super.init(frame: CGRect())
self.sourceView.isUserInteractionEnabled = false
self.addSubview(self.sourceView)
self.sourceView.addSubnode(self.referenceNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self, let contentView = strongSelf.componentView.view {
if highlighted {
contentView.layer.removeAnimation(forKey: "opacity")
contentView.alpha = 0.4
} else {
contentView.alpha = 1.0
contentView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.component?.action()
}
func update(component: ReferenceButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
let componentSize = self.componentView.update(
transition: transition,
component: component.content,
environment: {},
containerSize: availableSize
)
if let componentView = self.componentView.view {
if componentView.superview == nil {
self.referenceNode.view.addSubview(componentView)
}
transition.setFrame(view: componentView, frame: CGRect(origin: .zero, size: componentSize))
}
transition.setFrame(view: self.sourceView, frame: CGRect(origin: .zero, size: componentSize))
self.referenceNode.frame = CGRect(origin: .zero, size: componentSize)
return componentSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -1,79 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import SearchBarNode
import AppBundle
private let searchBarFont = Font.regular(17.0)
private extension SearchBarNodeTheme {
convenience init(navigationBarTheme: BrowserNavigationBarTheme) {
self.init(background: navigationBarTheme.backgroundColor, separator: .clear, inputFill: navigationBarTheme.searchBarFieldColor, primaryText: navigationBarTheme.searchBarTextColor, placeholder: navigationBarTheme.searchBarPlaceholderColor, inputIcon: navigationBarTheme.searchBarIconColor, inputClear: navigationBarTheme.searchBarClearColor, accent: navigationBarTheme.buttonColor, keyboard: navigationBarTheme.searchBarKeyboardColor)
}
}
final class BrowserNavigationBarSearchContentNode: ASDisplayNode, BrowserNavigationBarContentNode {
private var theme: BrowserNavigationBarTheme
private let strings: PresentationStrings
private var state: BrowserState
private var interaction: BrowserInteraction?
private let searchBar: SearchBarNode
init(theme: BrowserNavigationBarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?) {
self.theme = theme
self.strings = strings
self.state = state
self.interaction = interaction
let searchBarTheme = SearchBarNodeTheme(navigationBarTheme: self.theme)
self.searchBar = SearchBarNode(theme: searchBarTheme, strings: strings, fieldStyle: .modern)
self.searchBar.placeholderString = NSAttributedString(string: "Search on this page", font: searchBarFont, textColor: searchBarTheme.placeholder)
super.init()
self.backgroundColor = theme.backgroundColor
self.addSubnode(self.searchBar)
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
self?.interaction?.dismissSearch()
}
self.searchBar.textUpdated = { [weak self] query, _ in
self?.interaction?.updateSearchQuery(query)
}
}
override func didLoad() {
super.didLoad()
self.searchBar.activate()
}
func updateState(_ state: BrowserState) {
guard let searchState = state.search else {
return
}
self.searchBar.text = searchState.query
}
func updateTheme(_ theme: BrowserNavigationBarTheme) {
guard self.theme !== theme else {
return
}
self.theme = theme
self.backgroundColor = theme.backgroundColor
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(navigationBarTheme: self.theme), strings: self.strings)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.searchBar.updateLayout(boundingSize: size, leftInset: 0.0, rightInset: 0.0, transition: .immediate)
self.searchBar.frame = CGRect(origin: CGPoint(), size: size)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,357 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
import AccountContext
import BundleIconComponent
final class SearchBarContentComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let performAction: ActionSlot<BrowserScreen.Action>
init(
theme: PresentationTheme,
strings: PresentationStrings,
performAction: ActionSlot<BrowserScreen.Action>
) {
self.theme = theme
self.strings = strings
self.performAction = performAction
}
static func ==(lhs: SearchBarContentComponent, rhs: SearchBarContentComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
return true
}
final class View: UIView, UITextFieldDelegate {
private final class EmojiSearchTextField: 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: EmojiSearchTextField?
private var tapRecognizer: UITapGestureRecognizer?
private var params: Params?
private var component: SearchBarContentComponent?
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 = EmojiSearchTextField(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: SearchBarContentComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
self.update(theme: component.theme, strings: component.strings, size: availableSize, transition: transition)
self.activateTextInput()
return availableSize
}
public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: Transition) {
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.primaryTextColor
)),
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: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -1,165 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import AppBundle
private func toolbarContentNode(for state: BrowserState, currentContentNode: BrowserToolbarContentNode?, layoutMetrics: LayoutMetrics, theme: BrowserToolbarTheme, strings: PresentationStrings, interaction: BrowserInteraction?) -> BrowserToolbarContentNode? {
guard case .compact = layoutMetrics.widthClass else {
return nil
}
if let _ = state.search {
if let currentContentNode = currentContentNode as? BrowserToolbarSearchContentNode {
currentContentNode.updateState(state)
return currentContentNode
} else {
return BrowserToolbarSearchContentNode(theme: theme, strings: strings, state: state, interaction: interaction)
}
} else {
if let currentContentNode = currentContentNode as? BrowserToolbarNavigationContentNode {
currentContentNode.updateState(state)
return currentContentNode
} else {
return BrowserToolbarNavigationContentNode(theme: theme, strings: strings, state: state, interaction: interaction)
}
}
}
final class BrowserToolbarTheme {
let backgroundColor: UIColor
let separatorColor: UIColor
let buttonColor: UIColor
let disabledButtonColor: UIColor
init(backgroundColor: UIColor, separatorColor: UIColor, buttonColor: UIColor, disabledButtonColor: UIColor) {
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
self.buttonColor = buttonColor
self.disabledButtonColor = disabledButtonColor
}
}
protocol BrowserToolbarContentNode: ASDisplayNode {
init(theme: BrowserToolbarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?)
func updateState(_ state: BrowserState)
func updateTheme(_ theme: BrowserToolbarTheme)
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
}
private let toolbarHeight: CGFloat = 49.0
final class BrowserToolbar: ASDisplayNode {
private var theme: BrowserToolbarTheme
private let strings: PresentationStrings
private var state: BrowserState
var interaction: BrowserInteraction?
private let containerNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private var contentNode: BrowserToolbarContentNode?
private var validLayout: (CGFloat, UIEdgeInsets, LayoutMetrics, CGFloat)?
init(theme: BrowserToolbarTheme, strings: PresentationStrings, state: BrowserState) {
self.theme = theme
self.strings = strings
self.state = state
self.containerNode = ASDisplayNode()
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.separatorColor
super.init()
self.clipsToBounds = true
self.containerNode.backgroundColor = theme.backgroundColor
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.separatorNode)
}
func updateState(_ state: BrowserState) {
self.state = state
if let (width, insets, layoutMetrics, collapseTransition) = self.validLayout {
let _ = self.updateLayout(width: width, insets: insets, layoutMetrics: layoutMetrics, collapseTransition: collapseTransition, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
func updateTheme(_ theme: BrowserToolbarTheme) {
guard self.theme !== theme else {
return
}
self.theme = theme
self.containerNode.backgroundColor = theme.backgroundColor
self.separatorNode.backgroundColor = theme.separatorColor
self.contentNode?.updateTheme(theme)
}
func updateLayout(width: CGFloat, insets: UIEdgeInsets, layoutMetrics: LayoutMetrics, collapseTransition: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
self.validLayout = (width, insets, layoutMetrics, collapseTransition)
var dismissedContentNode: ASDisplayNode?
var immediatelyLayoutContentNodeAndAnimateAppearance = false
if let contentNode = toolbarContentNode(for: self.state, currentContentNode: self.contentNode, layoutMetrics: layoutMetrics, theme: self.theme, strings: self.strings, interaction: self.interaction) {
if contentNode !== self.contentNode {
dismissedContentNode = self.contentNode
immediatelyLayoutContentNodeAndAnimateAppearance = true
self.containerNode.insertSubnode(contentNode, belowSubnode: self.separatorNode)
self.contentNode = contentNode
}
} else {
dismissedContentNode = self.contentNode
self.contentNode = nil
}
let effectiveCollapseTransition = self.contentNode == nil ? 1.0 : collapseTransition
let height = toolbarHeight + insets.bottom
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: height * effectiveCollapseTransition), size: CGSize(width: width, height: height))
transition.updateFrame(node: self.containerNode, frame: containerFrame)
transition.updateFrame(node: self.separatorNode, frame: CGRect(x: 0.0, y: 0.0, width: width, height: UIScreenPixel))
let constrainedSize = CGSize(width: width - insets.left - insets.right, height: toolbarHeight)
if let contentNode = self.contentNode {
let contentNodeFrame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: constrainedSize)
contentNode.updateLayout(size: constrainedSize, transition: transition)
if immediatelyLayoutContentNodeAndAnimateAppearance {
contentNode.frame = contentNodeFrame.offsetBy(dx: 0.0, dy: contentNodeFrame.height)
contentNode.alpha = 0.0
}
transition.updateFrame(node: contentNode, frame: contentNodeFrame)
transition.updateAlpha(node: contentNode, alpha: 1.0)
}
if let dismissedContentNode = dismissedContentNode {
var frameCompleted = false
var alphaCompleted = false
let completed = { [weak self, weak dismissedContentNode] in
if let strongSelf = self, let dismissedContentNode = dismissedContentNode, strongSelf.contentNode === dismissedContentNode {
return
}
if frameCompleted && alphaCompleted {
dismissedContentNode?.removeFromSupernode()
}
}
let transitionTargetY = dismissedContentNode.frame.height
transition.updateFrame(node: dismissedContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: transitionTargetY), size: dismissedContentNode.frame.size), completion: { _ in
frameCompleted = true
completed()
})
transition.updateAlpha(node: dismissedContentNode, alpha: 0.0, completion: { _ in
alphaCompleted = true
completed()
})
}
return CGSize(width: width, height: height)
}
}

View File

@ -0,0 +1,368 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import BlurredBackgroundComponent
import BundleIconComponent
import TelegramPresentationData
final class BrowserToolbarComponent: CombinedComponent {
let backgroundColor: UIColor
let separatorColor: UIColor
let textColor: UIColor
let bottomInset: CGFloat
let sideInset: CGFloat
let item: AnyComponentWithIdentity<Empty>?
let collapseFraction: CGFloat
init(
backgroundColor: UIColor,
separatorColor: UIColor,
textColor: UIColor,
bottomInset: CGFloat,
sideInset: CGFloat,
item: AnyComponentWithIdentity<Empty>?,
collapseFraction: CGFloat
) {
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
self.textColor = textColor
self.bottomInset = bottomInset
self.sideInset = sideInset
self.item = item
self.collapseFraction = collapseFraction
}
static func ==(lhs: BrowserToolbarComponent, rhs: BrowserToolbarComponent) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.item != rhs.item {
return false
}
if lhs.collapseFraction != rhs.collapseFraction {
return false
}
return true
}
static var body: Body {
let background = Child(BlurredBackgroundComponent.self)
let separator = Child(Rectangle.self)
let centerItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
return { context in
let contentHeight: CGFloat = 49.0
let totalHeight = contentHeight + context.component.bottomInset
let offset = context.component.collapseFraction * totalHeight
let size = CGSize(width: context.availableSize.width, height: totalHeight)
let background = background.update(
component: BlurredBackgroundComponent(color: context.component.backgroundColor),
availableSize: CGSize(width: size.width, height: size.height),
transition: context.transition
)
let separator = separator.update(
component: Rectangle(color: context.component.separatorColor, height: UIScreenPixel),
availableSize: CGSize(width: size.width, height: size.height),
transition: context.transition
)
let item = context.component.item.flatMap { item in
return centerItems[item.id].update(
component: item.component,
availableSize: CGSize(width: context.availableSize.width - context.component.sideInset * 2.0, height: contentHeight),
transition: context.transition
)
}
context.add(background
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0 + offset))
)
context.add(separator
.position(CGPoint(x: size.width / 2.0, y: 0.0 + offset))
)
if let centerItem = item {
context.add(centerItem
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight / 2.0 + offset))
.appear(Transition.Appear({ _, view, transition in
transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: size.height), to: .zero, additive: true)
}))
.disappear(Transition.Disappear({ view, transition, completion in
transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: size.height), additive: true, completion: { _ in
completion()
})
}))
)
}
return size
}
}
}
final class NavigationToolbarContentComponent: CombinedComponent {
let textColor: UIColor
let canGoBack: Bool
let canGoForward: Bool
let performAction: ActionSlot<BrowserScreen.Action>
init(
textColor: UIColor,
canGoBack: Bool,
canGoForward: Bool,
performAction: ActionSlot<BrowserScreen.Action>
) {
self.textColor = textColor
self.canGoBack = canGoBack
self.canGoForward = canGoForward
self.performAction = performAction
}
static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool {
if lhs.textColor != rhs.textColor {
return false
}
if lhs.canGoBack != rhs.canGoBack {
return false
}
if lhs.canGoForward != rhs.canGoForward {
return false
}
return true
}
static var body: Body {
let back = Child(Button.self)
let forward = Child(Button.self)
let share = Child(Button.self)
let openIn = Child(Button.self)
return { context in
let availableSize = context.availableSize
let performAction = context.component.performAction
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 back = back.update(
component: Button(
content: AnyComponent(
BundleIconComponent(
name: "Instant View/Back",
tintColor: context.component.textColor
)
),
isEnabled: context.component.canGoBack,
action: {
performAction.invoke(.navigateBack)
}
).minSize(buttonSize),
availableSize: buttonSize,
transition: .easeInOut(duration: 0.2)
)
context.add(back
.position(CGPoint(x: sideInset + back.size.width / 2.0, y: availableSize.height / 2.0))
)
let forward = forward.update(
component: Button(
content: AnyComponent(
BundleIconComponent(
name: "Instant View/Forward",
tintColor: context.component.textColor
)
),
isEnabled: context.component.canGoForward,
action: {
performAction.invoke(.navigateForward)
}
).minSize(buttonSize),
availableSize: buttonSize,
transition: .easeInOut(duration: 0.2)
)
context.add(forward
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width / 2.0, y: availableSize.height / 2.0))
)
let share = share.update(
component: Button(
content: AnyComponent(
BundleIconComponent(
name: "Chat List/NavigationShare",
tintColor: context.component.textColor
)
),
action: {
performAction.invoke(.share)
}
).minSize(buttonSize),
availableSize: buttonSize,
transition: .easeInOut(duration: 0.2)
)
context.add(share
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width / 2.0, y: availableSize.height / 2.0))
)
let openIn = openIn.update(
component: Button(
content: AnyComponent(
BundleIconComponent(
name: "Chat/Context Menu/Browser",
tintColor: context.component.textColor
)
),
action: {
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))
)
return availableSize
}
}
}
final class SearchToolbarContentComponent: CombinedComponent {
let strings: PresentationStrings
let textColor: UIColor
let index: Int
let count: Int
let isEmpty: Bool
let performAction: ActionSlot<BrowserScreen.Action>
init(
strings: PresentationStrings,
textColor: UIColor,
index: Int,
count: Int,
isEmpty: Bool,
performAction: ActionSlot<BrowserScreen.Action>
) {
self.strings = strings
self.textColor = textColor
self.index = index
self.count = count
self.isEmpty = isEmpty
self.performAction = performAction
}
static func ==(lhs: SearchToolbarContentComponent, rhs: SearchToolbarContentComponent) -> Bool {
if lhs.strings !== rhs.strings {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.count != rhs.count {
return false
}
if lhs.isEmpty != rhs.isEmpty {
return false
}
return true
}
static var body: Body {
let down = Child(Button.self)
let up = Child(Button.self)
let text = Child(Text.self)
return { context in
let availableSize = context.availableSize
let performAction = context.component.performAction
let sideInset: CGFloat = 3.0
let buttonSize = CGSize(width: 50.0, height: availableSize.height)
let down = down.update(
component: Button(
content: AnyComponent(
BundleIconComponent(
name: "Chat/Input/Search/DownButton",
tintColor: context.component.textColor
)
),
isEnabled: context.component.count > 0,
action: {
performAction.invoke(.scrollToNextSearchResult)
}
).minSize(buttonSize),
availableSize: buttonSize,
transition: .easeInOut(duration: 0.2)
)
context.add(down
.position(CGPoint(x: availableSize.width - sideInset - down.size.width / 2.0, y: availableSize.height / 2.0))
)
let up = up.update(
component: Button(
content: AnyComponent(
BundleIconComponent(
name: "Chat/Input/Search/UpButton",
tintColor: context.component.textColor
)
),
isEnabled: context.component.count > 0,
action: {
performAction.invoke(.scrollToPreviousSearchResult)
}
).minSize(buttonSize),
availableSize: buttonSize,
transition: .easeInOut(duration: 0.2)
)
context.add(up
.position(CGPoint(x: availableSize.width - sideInset - down.size.width + 7.0 - up.size.width / 2.0, y: availableSize.height / 2.0))
)
let currentText: String
if context.component.isEmpty {
currentText = ""
} else if context.component.count == 0 {
currentText = context.component.strings.Conversation_SearchNoResults
} else {
currentText = context.component.strings.Items_NOfM("\(context.component.index + 1)", "\(context.component.count)").string
}
let text = text.update(
component: Text(
text: currentText,
font: Font.regular(15.0),
color: context.component.textColor
),
availableSize: availableSize,
transition: .easeInOut(duration: 0.2)
)
context.add(text
.position(CGPoint(x: availableSize.width - sideInset - down.size.width - up.size.width - text.size.width / 2.0, y: availableSize.height / 2.0))
)
return availableSize
}
}
}

View File

@ -1,103 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import AppBundle
final class BrowserToolbarNavigationContentNode: ASDisplayNode, BrowserToolbarContentNode {
private var theme: BrowserToolbarTheme
private var state: BrowserState
private var interaction: BrowserInteraction?
private let backButton: HighlightableButtonNode
private let forwardButton: HighlightableButtonNode
private let shareButton: HighlightableButtonNode
private let minimizeButton: HighlightableButtonNode
private var validLayout: CGSize?
init(theme: BrowserToolbarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?) {
self.theme = theme
self.state = state
self.interaction = interaction
self.backButton = HighlightableButtonNode()
self.backButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Back"), color: theme.buttonColor), for: [])
self.backButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Back"), color: theme.disabledButtonColor), for: [.disabled])
self.forwardButton = HighlightableButtonNode()
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Forward"), color: theme.buttonColor), for: [])
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Forward"), color: theme.disabledButtonColor), for: [.disabled])
self.shareButton = HighlightableButtonNode()
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.buttonColor), for: [])
self.minimizeButton = HighlightableButtonNode()
self.minimizeButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Minimize"), color: theme.buttonColor), for: [])
super.init()
self.addSubnode(self.backButton)
self.addSubnode(self.forwardButton)
self.addSubnode(self.shareButton)
self.addSubnode(self.minimizeButton)
self.backButton.isEnabled = false
self.forwardButton.isEnabled = false
self.backButton.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
self.forwardButton.addTarget(self, action: #selector(self.forwardPressed), forControlEvents: .touchUpInside)
self.shareButton.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside)
self.minimizeButton.addTarget(self, action: #selector(self.minimizePressed), forControlEvents: .touchUpInside)
}
func updateState(_ state: BrowserState) {
self.state = state
self.backButton.isEnabled = state.content?.canGoBack ?? false
self.forwardButton.isEnabled = state.content?.canGoForward ?? false
}
func updateTheme(_ theme: BrowserToolbarTheme) {
guard self.theme !== theme else {
return
}
self.theme = theme
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.validLayout == nil
self.validLayout = size
var transition = transition
if isFirstLayout {
transition = .immediate
}
let buttons = [self.backButton, self.forwardButton, self.shareButton, self.minimizeButton]
let sideInset: CGFloat = 5.0
let buttonSize = CGSize(width: 50.0, height: size.height)
let spacing: CGFloat = (size.width - buttonSize.width * CGFloat(buttons.count) - sideInset * 2.0) / CGFloat(buttons.count - 1)
var offset: CGFloat = sideInset
for button in buttons {
transition.updateFrame(node: button, frame: CGRect(origin: CGPoint(x: offset, y: 0.0), size: buttonSize))
offset += buttonSize.width + spacing
}
}
@objc private func backPressed() {
self.interaction?.navigateBack()
}
@objc private func forwardPressed() {
self.interaction?.navigateForward()
}
@objc private func sharePressed() {
self.interaction?.share()
}
@objc private func minimizePressed() {
self.interaction?.minimize()
}
}

View File

@ -1,95 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import AppBundle
final class BrowserToolbarSearchContentNode: ASDisplayNode, BrowserToolbarContentNode {
private var theme: BrowserToolbarTheme
private let strings: PresentationStrings
private var state: BrowserState
private var interaction: BrowserInteraction?
private let upButton: HighlightableButtonNode
private let downButton: HighlightableButtonNode
private let resultsNode: ImmediateTextNode
private var validLayout: CGSize?
init(theme: BrowserToolbarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?) {
self.theme = theme
self.strings = strings
self.state = state
self.interaction = interaction
self.upButton = HighlightableButtonNode()
self.upButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.buttonColor), for: .normal)
self.upButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.disabledButtonColor), for: .disabled)
self.downButton = HighlightableButtonNode()
self.downButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.buttonColor), for: .normal)
self.downButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.disabledButtonColor), for: .disabled)
self.resultsNode = ImmediateTextNode()
super.init()
self.addSubnode(self.upButton)
self.addSubnode(self.downButton)
self.addSubnode(self.resultsNode)
self.upButton.addTarget(self, action: #selector(self.upPressed), forControlEvents: .touchUpInside)
self.downButton.addTarget(self, action: #selector(self.downPressed), forControlEvents: .touchUpInside)
}
func updateState(_ state: BrowserState) {
self.state = state
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
}
func updateTheme(_ theme: BrowserToolbarTheme) {
guard self.theme !== theme else {
return
}
self.theme = theme
self.upButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.buttonColor), for: .normal)
self.upButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.disabledButtonColor), for: .disabled)
self.downButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.buttonColor), for: .normal)
self.downButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.disabledButtonColor), for: .disabled)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
let buttonSize = CGSize(width: 40.0, height: size.height)
let resultsText: String
if let results = self.state.search?.results {
if results.1 > 0 {
resultsText = self.strings.Items_NOfM("\(results.0 + 1)", "\(results.1)").string
} else {
resultsText = self.strings.Conversation_SearchNoResults
}
} else {
resultsText = ""
}
self.resultsNode.attributedText = NSAttributedString(string: resultsText, font: Font.regular(15.0), textColor: self.theme.buttonColor, paragraphAlignment: .natural)
let resultsSize = self.resultsNode.updateLayout(size)
self.resultsNode.frame = CGRect(origin: CGPoint(x: size.width - 48.0 - 43.0 - resultsSize.width - 12.0, y: floor((size.height - resultsSize.height) / 2.0)), size: resultsSize)
self.downButton.frame = CGRect(origin: CGPoint(x: size.width - 48.0, y: 0.0), size: buttonSize)
self.upButton.frame = CGRect(origin: CGPoint(x: size.width - 48.0 - 43.0, y: 0.0), size: buttonSize)
}
@objc private func upPressed() {
self.interaction?.scrollToPreviousSearchResult()
}
@objc private func downPressed() {
self.interaction?.scrollToNextSearchResult()
}
}

View File

@ -1,17 +1,16 @@
import Foundation import Foundation
import UIKit import UIKit
import AsyncDisplayKit import ComponentFlow
import TelegramCore import TelegramCore
import Postbox import Postbox
import SwiftSignalKit import SwiftSignalKit
import Display
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import AccountContext import AccountContext
import WebKit import WebKit
import AppBundle import AppBundle
final class BrowserWebContent: ASDisplayNode, BrowserContent { final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
private let webView: WKWebView private let webView: WKWebView
private var _state: BrowserContentState private var _state: BrowserContentState
@ -21,6 +20,8 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
return self.statePromise.get() return self.statePromise.get()
} }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
init(url: String) { init(url: String) {
let configuration = WKWebViewConfiguration() let configuration = WKWebViewConfiguration()
@ -40,17 +41,24 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
title = parsedUrl.host ?? "" title = parsedUrl.host ?? ""
} }
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, isInstant: false) self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .webPage)
self.statePromise = Promise<BrowserContentState>(self._state) self.statePromise = Promise<BrowserContentState>(self._state)
super.init() super.init(frame: .zero)
self.webView.allowsBackForwardNavigationGestures = true self.webView.allowsBackForwardNavigationGestures = true
self.webView.scrollView.delegate = self
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: [], context: nil)
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: [], context: nil)
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil)
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack), options: [], context: nil)
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil)
self.addSubview(self.webView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
} }
deinit { deinit {
@ -61,12 +69,6 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward)) self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward))
} }
override func didLoad() {
super.didLoad()
self.view.addSubview(self.webView)
}
func setFontSize(_ fontSize: CGFloat) { func setFontSize(_ fontSize: CGFloat) {
let js = "document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust='\(Int(fontSize * 100.0))%'" let js = "document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust='\(Int(fontSize * 100.0))%'"
self.webView.evaluateJavaScript(js, completionHandler: nil) self.webView.evaluateJavaScript(js, completionHandler: nil)
@ -109,7 +111,7 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
}) })
} }
var previousQuery: String? private var previousQuery: String?
func setSearch(_ query: String?, completion: ((Int) -> Void)?) { func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
guard self.previousQuery != query else { guard self.previousQuery != query else {
return return
@ -182,8 +184,15 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true)
} }
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0))) var scrollInsets = insets
scrollInsets.top = 0.0
if self.webView.scrollView.contentInset != insets {
self.webView.scrollView.contentInset = scrollInsets
self.webView.scrollView.scrollIndicatorInsets = scrollInsets
}
self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating)
transition.setFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top)))
} }
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
@ -195,15 +204,63 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
if keyPath == "title" { if keyPath == "title" {
updateState { $0.withUpdatedTitle(self.webView.title ?? "") } updateState { $0.withUpdatedTitle(self.webView.title ?? "") }
} else if keyPath == "url" { } else if keyPath == "URL" {
updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") } updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") }
self.didSetupSearch = false self.didSetupSearch = false
} else if keyPath == "estimatedProgress" { } else if keyPath == "estimatedProgress" {
updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) }
} else if keyPath == "canGoBack" { } else if keyPath == "canGoBack" {
updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) }
self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack
} else if keyPath == "canGoForward" { } else if keyPath == "canGoForward" {
updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) }
} }
} }
private struct ScrollingOffsetState: Equatable {
var value: CGFloat
var isDraggingOrDecelerating: Bool
}
private var previousScrollingOffset: ScrollingOffsetState?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrollingOffset(isReset: false, transition: .immediate)
}
private func snapScrollingOffsetToInsets() {
let transition = Transition(animation: .curve(duration: 0.4, curve: .spring))
self.updateScrollingOffset(isReset: false, transition: transition)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.snapScrollingOffsetToInsets()
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.snapScrollingOffsetToInsets()
}
private func updateScrollingOffset(isReset: Bool, transition: Transition) {
let scrollView = self.webView.scrollView
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
if let previousScrollingOffsetValue = self.previousScrollingOffset {
let currentBounds = scrollView.bounds
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY)
let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value
self.onScrollingUpdate(ContentScrollingUpdate(
relativeOffset: relativeOffset,
absoluteOffsetToTopEdge: offsetToTopEdge,
absoluteOffsetToBottomEdge: offsetToBottomEdge,
isReset: isReset,
isInteracting: isInteracting,
transition: transition
))
}
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting)
}
} }

View File

@ -899,18 +899,26 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
return settings return settings
} }
var isCompact = false
if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass {
isCompact = true
}
let _ = (settings let _ = (settings
|> deliverOnMainQueue).start(next: { settings in |> deliverOnMainQueue).start(next: { settings in
if settings.defaultWebBrowser == nil { if settings.defaultWebBrowser == nil {
// let controller = BrowserScreen(context: context, subject: .webPage(parsedUrl.absoluteString)) if isCompact {
// navigationController?.pushViewController(controller) let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString))
if let window = navigationController?.view.window { navigationController?.pushViewController(controller)
let controller = SFSafariViewController(url: parsedUrl)
controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor
window.rootViewController?.present(controller, animated: true)
} else { } else {
context.sharedContext.applicationBindings.openUrl(parsedUrl.absoluteString) if let window = navigationController?.view.window {
let controller = SFSafariViewController(url: parsedUrl)
controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor
window.rootViewController?.present(controller, animated: true)
} else {
context.sharedContext.applicationBindings.openUrl(parsedUrl.absoluteString)
}
} }
} else { } else {
let openInOptions = availableOpenInOptions(context: context, item: .url(url: url)) let openInOptions = availableOpenInOptions(context: context, item: .url(url: url))