mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Initial in-app browser implementation
This commit is contained in:
parent
fa0130bf8c
commit
bc1930be7c
@ -20,6 +20,12 @@ swift_library(
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/InstantPageUI:InstantPageUI",
|
||||
"//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:public",
|
||||
|
@ -1,51 +1,86 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
|
||||
final class BrowserContentState {
|
||||
final class BrowserContentState: Equatable {
|
||||
enum ContentType: Equatable {
|
||||
case webPage
|
||||
case instantPage
|
||||
}
|
||||
|
||||
let title: String
|
||||
let url: String
|
||||
let estimatedProgress: Double
|
||||
let isInstant: Bool
|
||||
let contentType: ContentType
|
||||
|
||||
var canGoBack: 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.url = url
|
||||
self.estimatedProgress = estimatedProgress
|
||||
self.isInstant = isInstant
|
||||
self.contentType = contentType
|
||||
self.canGoBack = canGoBack
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 onScrollingUpdate: (ContentScrollingUpdate) -> Void { get set }
|
||||
|
||||
func navigateBack()
|
||||
func navigateForward()
|
||||
|
||||
@ -58,5 +93,30 @@ protocol BrowserContent: ASDisplayNode {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,12 @@ import AppBundle
|
||||
import ContextUI
|
||||
|
||||
final class BrowserFontSizeContextMenuItem: ContextMenuCustomItem {
|
||||
private let value: CGFloat
|
||||
private let decrease: () -> CGFloat
|
||||
private let increase: () -> CGFloat
|
||||
private let value: Int32
|
||||
private let decrease: () -> Int32
|
||||
private let increase: () -> Int32
|
||||
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.decrease = decrease
|
||||
self.increase = increase
|
||||
@ -46,17 +46,17 @@ private final class BrowserFontSizeContextMenuItemNode: ASDisplayNode, ContextMe
|
||||
private let leftSeparatorNode: ASDisplayNode
|
||||
private let rightSeparatorNode: ASDisplayNode
|
||||
|
||||
var value: CGFloat = 1.0 {
|
||||
var value: Int32 = 100 {
|
||||
didSet {
|
||||
self.updateValue()
|
||||
}
|
||||
}
|
||||
|
||||
private let decrease: () -> CGFloat
|
||||
private let increase: () -> CGFloat
|
||||
private let decrease: () -> Int32
|
||||
private let increase: () -> Int32
|
||||
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.value = value
|
||||
self.decrease = decrease
|
||||
@ -198,14 +198,14 @@ private final class BrowserFontSizeContextMenuItemNode: ASDisplayNode, ContextMe
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
self.leftButtonNode.isEnabled = self.value > 0.5
|
||||
self.leftButtonNode.isEnabled = self.value > 50
|
||||
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.centerButtonNode.isEnabled = self.value != 1.0
|
||||
self.centerButtonNode.isEnabled = self.value != 100
|
||||
self.centerTextNode.alpha = self.centerButtonNode.isEnabled ? 1.0 : 0.4
|
||||
}
|
||||
|
||||
@ -255,7 +255,7 @@ private final class BrowserFontSizeContextMenuItemNode: ASDisplayNode, ContextMe
|
||||
|
||||
@objc private func centerPressed() {
|
||||
self.reset()
|
||||
self.value = 1.0
|
||||
self.value = 100
|
||||
}
|
||||
|
||||
func canBeHighlighted() -> Bool {
|
||||
|
@ -5,14 +5,877 @@
|
||||
//import Postbox
|
||||
//import SwiftSignalKit
|
||||
//import Display
|
||||
//import ComponentFlow
|
||||
//import TelegramPresentationData
|
||||
//import TelegramUIPreferences
|
||||
//import AccountContext
|
||||
//import AppBundle
|
||||
//import InstantPageUI
|
||||
//
|
||||
//final class BrowserInstantPageContent: ASDisplayNode, BrowserContent {
|
||||
// private let instantPageNode: InstantPageContentNode
|
||||
//final class InstantPageView: UIView, UIScrollViewDelegate {
|
||||
// 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 let statePromise: Promise<BrowserContentState>
|
||||
@ -28,7 +891,6 @@
|
||||
// self.webPage = webPage
|
||||
//
|
||||
// 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
|
||||
// if case let .Loaded(content) = webPage.content {
|
||||
@ -37,12 +899,16 @@
|
||||
// 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)
|
||||
//
|
||||
// super.init()
|
||||
//
|
||||
// self.addSubnode(self.instantPageNode)
|
||||
//
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// fatalError("init(coder:) has not been implemented")
|
||||
// }
|
||||
//
|
||||
// func navigateBack() {
|
||||
@ -78,14 +944,14 @@
|
||||
// }
|
||||
//
|
||||
// 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)
|
||||
// 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)
|
||||
// //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 {
|
||||
// self.initialized = true
|
||||
// self.instantPageNode.updateWebPage(self.webPage, anchor: nil)
|
||||
// }
|
||||
//// 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.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)))
|
||||
////
|
||||
//// if !self.initialized {
|
||||
//// self.initialized = true
|
||||
//// self.instantPageNode.updateWebPage(self.webPage, anchor: nil)
|
||||
//// }
|
||||
// }
|
||||
//}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
443
submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift
Normal file
443
submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
357
submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift
Normal file
357
submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
368
submodules/BrowserUI/Sources/BrowserToolbarComponent.swift
Normal file
368
submodules/BrowserUI/Sources/BrowserToolbarComponent.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,17 +1,16 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import WebKit
|
||||
import AppBundle
|
||||
|
||||
final class BrowserWebContent: ASDisplayNode, BrowserContent {
|
||||
final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
|
||||
private let webView: WKWebView
|
||||
|
||||
private var _state: BrowserContentState
|
||||
@ -21,6 +20,8 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
|
||||
return self.statePromise.get()
|
||||
}
|
||||
|
||||
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
|
||||
|
||||
init(url: String) {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
|
||||
@ -40,17 +41,24 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
|
||||
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)
|
||||
|
||||
super.init()
|
||||
super.init(frame: .zero)
|
||||
|
||||
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.url), 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.canGoForward), options: [], context: nil)
|
||||
|
||||
self.addSubview(self.webView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -61,12 +69,6 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
|
||||
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward))
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addSubview(self.webView)
|
||||
}
|
||||
|
||||
func setFontSize(_ fontSize: CGFloat) {
|
||||
let js = "document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust='\(Int(fontSize * 100.0))%'"
|
||||
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)?) {
|
||||
guard self.previousQuery != query else {
|
||||
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)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||
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)))
|
||||
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
|
||||
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?) {
|
||||
@ -195,15 +204,63 @@ final class BrowserWebContent: ASDisplayNode, BrowserContent {
|
||||
|
||||
if keyPath == "title" {
|
||||
updateState { $0.withUpdatedTitle(self.webView.title ?? "") }
|
||||
} else if keyPath == "url" {
|
||||
} else if keyPath == "URL" {
|
||||
updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") }
|
||||
self.didSetupSearch = false
|
||||
} else if keyPath == "estimatedProgress" {
|
||||
updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) }
|
||||
} else if keyPath == "canGoBack" {
|
||||
updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) }
|
||||
self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack
|
||||
} else if keyPath == "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)
|
||||
}
|
||||
}
|
||||
|
@ -899,18 +899,26 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
||||
return settings
|
||||
}
|
||||
|
||||
var isCompact = false
|
||||
if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass {
|
||||
isCompact = true
|
||||
}
|
||||
|
||||
let _ = (settings
|
||||
|> deliverOnMainQueue).start(next: { settings in
|
||||
if settings.defaultWebBrowser == nil {
|
||||
// let controller = BrowserScreen(context: context, subject: .webPage(parsedUrl.absoluteString))
|
||||
// navigationController?.pushViewController(controller)
|
||||
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)
|
||||
if isCompact {
|
||||
let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString))
|
||||
navigationController?.pushViewController(controller)
|
||||
} 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 {
|
||||
let openInOptions = availableOpenInOptions(context: context, item: .url(url: url))
|
||||
|
Loading…
x
Reference in New Issue
Block a user