Swiftgram/submodules/BrowserUI/Sources/BrowserScreen.swift
2022-12-25 10:33:53 +04:00

521 lines
23 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import Postbox
import SwiftSignalKit
import Display
import TelegramPresentationData
import TelegramUIPreferences
import ContextUI
import AccountContext
import ShareController
import OpenInExternalAppUI
private final class InstantPageContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = false
let ignoreContentTouches: Bool = false
let blurBackground: Bool = false
private weak var navigationBar: BrowserNavigationBar?
init(navigationBar: BrowserNavigationBar) {
self.navigationBar = navigationBar
}
func takeView() -> ContextControllerTakeViewInfo? {
guard let navigationBar = self.navigationBar else {
return nil
}
return ContextControllerTakeViewInfo(containingItem: .node(navigationBar.contextSourceNode), contentAreaInScreenSpace: navigationBar.convert(navigationBar.contextSourceNode.frame.offsetBy(dx: 0.0, dy: 40.0), to: nil))
}
func putBack() -> ContextControllerPutBackViewInfo? {
guard let navigationBar = self.navigationBar else {
return nil
}
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: navigationBar.convert(navigationBar.contextSourceNode.frame.offsetBy(dx: 0.0, dy: 40.0), to: nil))
}
}
public enum BrowserSubject {
// case instantPage(TelegramMediaWebpage, String)
case webPage(String)
var isInstant: Bool {
return false
// if case .instantPage = self {
// return true
// } else {
// return false
// }
}
}
public final class BrowserScreen: ViewController {
private let context: AccountContext
private let subject: BrowserSubject
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
public init(context: AccountContext, subject: BrowserSubject) {
self.context = context
self.subject = subject
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .modal
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else {
return
}
strongSelf.presentationData = presentationData
(strongSelf.displayNode as! BrowserScreenNode).updatePresentationData(presentationData)
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = BrowserScreenNode(context: self.context, presentationData: self.presentationData, subject: self.subject, titleUpdated: { [weak self] title in
self?.title = title
})
(self.displayNode as! BrowserScreenNode).present = { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}
(self.displayNode as! BrowserScreenNode).minimize = {
// if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController {
// navigationController.minimizeViewController(strongSelf, animated: true)
// }
}
(self.displayNode as! BrowserScreenNode).close = { [weak self] in
self?.dismiss()
}
self.displayNodeDidLoad()
self._ready.set(.single(true))
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let navigationHeight: CGFloat
if case .modal = self.navigationPresentation {
navigationHeight = 56.0
} else {
navigationHeight = 44.0
}
(self.displayNode as! BrowserScreenNode).containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
}
}
struct BrowserSearchState {
var query: String
var results: (Int, Int)?
func withUpdatedQuery(_ query: String) -> BrowserSearchState {
return BrowserSearchState(query: query, results: self.results)
}
func withUpdatedResults(_ results: (Int, Int)?) -> BrowserSearchState {
return BrowserSearchState(query: self.query, results: results)
}
}
struct BrowserPresentationState {
let fontSize: CGFloat
let forceSerif: Bool
func withUpdatedFontSize(_ fontSize: CGFloat) -> BrowserPresentationState {
return BrowserPresentationState(fontSize: fontSize, forceSerif: self.forceSerif)
}
func withUpdatedForceSerif(_ forceSerif: Bool) -> BrowserPresentationState {
return BrowserPresentationState(fontSize: self.fontSize, forceSerif: forceSerif)
}
}
final class BrowserState {
let content: BrowserContentState?
let presentation: BrowserPresentationState
let search: BrowserSearchState?
init(content: BrowserContentState? = nil, presentation: BrowserPresentationState, search: BrowserSearchState? = nil) {
self.content = content
self.presentation = presentation
self.search = search
}
func withUpdatedContent(_ content: BrowserContentState) -> BrowserState {
return BrowserState(content: content, presentation: self.presentation, search: self.search)
}
func withUpdatedPresentation(_ presentation: BrowserPresentationState) -> BrowserState {
return BrowserState(content: content, presentation: presentation, search: self.search)
}
func withUpdatedSearch(_ search: BrowserSearchState?) -> BrowserState {
return BrowserState(content: self.content, presentation: self.presentation, search: search)
}
}
private final class BrowserTheme {
let backgroundColor: UIColor
let navigationBar: BrowserNavigationBarTheme
let toolbar: BrowserToolbarTheme
init(backgroundColor: UIColor, navigationBar: BrowserNavigationBarTheme, toolbar: BrowserToolbarTheme) {
self.backgroundColor = backgroundColor
self.navigationBar = navigationBar
self.toolbar = toolbar
}
}
extension BrowserTheme {
convenience init(presentationTheme: PresentationTheme) {
self.init(backgroundColor: presentationTheme.list.plainBackgroundColor,
navigationBar: BrowserNavigationBarTheme(
backgroundColor: presentationTheme.rootController.navigationBar.opaqueBackgroundColor,
separatorColor: presentationTheme.rootController.navigationBar.separatorColor,
primaryTextColor: presentationTheme.rootController.navigationBar.primaryTextColor,
loadingProgressColor: presentationTheme.rootController.navigationBar.accentTextColor,
readingProgressColor: presentationTheme.rootController.navigationBar.segmentedBackgroundColor,
buttonColor: presentationTheme.rootController.navigationBar.primaryTextColor,
disabledButtonColor: presentationTheme.chat.inputPanel.inputTextColor.withAlphaComponent(0.3),
searchBarFieldColor: presentationTheme.rootController.navigationSearchBar.inputFillColor,
searchBarTextColor: presentationTheme.rootController.navigationSearchBar.inputTextColor,
searchBarPlaceholderColor: presentationTheme.rootController.navigationSearchBar.inputPlaceholderTextColor,
searchBarIconColor: presentationTheme.rootController.navigationSearchBar.inputIconColor,
searchBarClearColor: presentationTheme.rootController.navigationSearchBar.inputClearButtonColor,
searchBarKeyboardColor: presentationTheme.rootController.keyboardColor
),
toolbar: BrowserToolbarTheme(
backgroundColor: presentationTheme.chat.inputPanel.panelBackgroundColor,
separatorColor: presentationTheme.chat.inputPanel.panelSeparatorColor,
buttonColor: presentationTheme.chat.inputPanel.inputTextColor,
disabledButtonColor: presentationTheme.chat.inputPanel.inputTextColor.withAlphaComponent(0.3)))
}
}
private final class BrowserScreenNode: ViewControllerTracingNode {
private let context: AccountContext
private var presentationData: PresentationData
private let subject: BrowserSubject
private var interaction: BrowserInteraction?
private var browserState: BrowserState
private var browserStatePromise: Promise<BrowserState>
private var stateDisposable: Disposable?
private let navigationBar: BrowserNavigationBar
private let toolbar: BrowserToolbar
private let contentContainerNode: ASDisplayNode
private var content: BrowserContent?
private var contentStateDisposable: Disposable?
private var validLayout: (ContainerViewLayout, CGFloat)?
var present: ((ViewController, Any?) -> Void)?
var minimize: (() -> Void)?
var close: (() -> Void)?
let titleUpdated: (String) -> Void
init(context: AccountContext, presentationData: PresentationData, subject: BrowserSubject, titleUpdated: @escaping ((String) -> Void)) {
self.context = context
self.presentationData = presentationData
self.subject = subject
self.titleUpdated = titleUpdated
self.browserState = BrowserState(content: BrowserContentState(title: "", url: "", estimatedProgress: 0.0, isInstant: subject.isInstant), presentation: BrowserPresentationState(fontSize: 1.0, forceSerif: false))
self.browserStatePromise = Promise<BrowserState>(self.browserState)
let theme = BrowserTheme(presentationTheme: self.presentationData.theme)
self.navigationBar = BrowserNavigationBar(theme: theme.navigationBar, strings: self.presentationData.strings, state: self.browserState)
self.toolbar = BrowserToolbar(theme: theme.toolbar, strings: self.presentationData.strings, state: self.browserState)
self.contentContainerNode = ASDisplayNode()
super.init()
self.backgroundColor = theme.backgroundColor
self.addSubnode(self.contentContainerNode)
self.addSubnode(self.toolbar)
self.addSubnode(self.navigationBar)
self.navigationBar.close = { [weak self] in
self?.close?()
}
self.navigationBar.openSettings = { [weak self] in
self?.openSettings()
}
self.navigationBar.scrollToTop = { [weak self] in
self?.scrollToTop()
}
self.stateDisposable = (self.browserStatePromise.get()
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.titleUpdated(state.content?.title ?? "")
strongSelf.navigationBar.updateState(state)
strongSelf.toolbar.updateState(state)
if let search = state.search, !search.query.isEmpty {
strongSelf.content?.setSearch(search.query, completion: { [weak self] count in
if let strongSelf = self {
strongSelf.updateState { $0.withUpdatedSearch($0.search?.withUpdatedResults((0, count))) }
}
})
} else {
strongSelf.content?.setSearch(nil, completion: nil)
}
})
let content: BrowserContent
switch self.subject {
case let .webPage(url):
content = BrowserWebContent(url: url)
// case let .instantPage(webPage, url):
// content = BrowserInstantPageContent(context: context, webPage: webPage, url: url)
}
self.contentContainerNode.addSubnode(content)
self.content = content
self.contentStateDisposable = (content.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.browserState = strongSelf.browserState.withUpdatedContent(state)
strongSelf.browserStatePromise.set(.single(strongSelf.browserState))
if strongSelf.isNodeLoaded {
strongSelf.content?.view.disablesInteractiveTransitionGestureRecognizer = state.canGoBack
}
})
self.interaction = BrowserInteraction(navigateBack: { [weak self] in
self?.content?.navigateBack()
}, navigateForward: { [weak self] in
self?.content?.navigateForward()
}, share: { [weak self] in
if let strongSelf = self, let url = strongSelf.browserState.content?.url {
let controller = ShareController(context: context, subject: .url(url))
strongSelf.present?(controller, nil)
}
}, minimize: { [weak self] in
self?.minimize?()
}, openSearch: { [weak self] in
self?.updateState { $0.withUpdatedSearch(BrowserSearchState(query: "", results: nil)) }
}, updateSearchQuery: { [weak self] text in
self?.updateState { $0.withUpdatedSearch(BrowserSearchState(query: text, results: nil)) }
}, dismissSearch: { [weak self] in
self?.updateState { $0.withUpdatedSearch(nil) }
}, scrollToPreviousSearchResult: { [weak self] in
self?.content?.scrollToPreviousSearchResult(completion: { [weak self] index, count in
if let strongSelf = self {
strongSelf.updateState { $0.withUpdatedSearch($0.search?.withUpdatedResults((index, count))) }
}
})
}, scrollToNextSearchResult: { [weak self] in
self?.content?.scrollToNextSearchResult(completion: { [weak self] index, count in
if let strongSelf = self {
strongSelf.updateState { $0.withUpdatedSearch($0.search?.withUpdatedResults((index, count))) }
}
})
}, decreaseFontSize: { [weak self] in
if let strongSelf = self {
strongSelf.updateState { $0.withUpdatedPresentation($0.presentation.withUpdatedFontSize(max(0.5, $0.presentation.fontSize - 0.25))) }
strongSelf.content?.setFontSize(strongSelf.browserState.presentation.fontSize)
}
}, increaseFontSize: { [weak self] in
if let strongSelf = self {
strongSelf.updateState { $0.withUpdatedPresentation($0.presentation.withUpdatedFontSize(min(2.0, $0.presentation.fontSize + 0.25))) }
strongSelf.content?.setFontSize(strongSelf.browserState.presentation.fontSize)
}
}, resetFontSize: { [weak self] in
if let strongSelf = self {
strongSelf.updateState { $0.withUpdatedPresentation($0.presentation.withUpdatedFontSize(1.0)) }
strongSelf.content?.setFontSize(strongSelf.browserState.presentation.fontSize)
}
}, updateForceSerif: { [weak self] force in
if let strongSelf = self {
strongSelf.updateState { $0.withUpdatedPresentation($0.presentation.withUpdatedForceSerif(force)) }
strongSelf.content?.setForceSerif(force)
}
})
self.navigationBar.interaction = self.interaction
self.toolbar.interaction = self.interaction
}
deinit {
self.stateDisposable?.dispose()
self.contentStateDisposable?.dispose()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
let theme = BrowserTheme(presentationTheme: self.presentationData.theme)
self.backgroundColor = theme.backgroundColor
self.navigationBar.updateTheme(theme.navigationBar)
self.toolbar.updateTheme(theme.toolbar)
}
func updateState(_ f: (BrowserState) -> BrowserState) {
self.browserState = f(self.browserState)
self.browserStatePromise.set(.single(self.browserState))
}
func openSettings() {
self.view.endEditing(true)
let checkIcon: (PresentationTheme) -> UIImage? = { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Check"), color: theme.contextMenu.primaryColor) }
let emptyIcon: (PresentationTheme) -> UIImage? = { _ in
return nil
}
let settings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings])
|> take(1)
|> map { sharedData -> WebBrowserSettings in
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) {
return current
} else {
return WebBrowserSettings.defaultSettings
}
}
let _ = (settings
|> deliverOnMainQueue).start(next: { [weak self] settings in
guard let strongSelf = self else {
return
}
let forceSerif = strongSelf.browserState.presentation.forceSerif
let source: ContextContentSource = .extracted(InstantPageContextExtractedContentSource(navigationBar: strongSelf.navigationBar))
let fontItem = BrowserFontSizeContextMenuItem(value: strongSelf.browserState.presentation.fontSize, decrease: { [weak self] in
self?.interaction?.decreaseFontSize()
return self?.browserState.presentation.fontSize ?? 1.0
}, increase: { [weak self] in
self?.interaction?.increaseFontSize()
return self?.browserState.presentation.fontSize ?? 1.0
}, reset: { [weak self] in
self?.interaction?.resetFontSize()
})
var defaultWebBrowser: String? = settings.defaultWebBrowser
if defaultWebBrowser == nil || defaultWebBrowser == "inAppSafari" {
defaultWebBrowser = "safari"
}
let url = strongSelf.browserState.content?.url ?? ""
let openInOptions = availableOpenInOptions(context: strongSelf.context, item: .url(url: url))
let openInTitle: String
let openInUrl: String
if let option = openInOptions.first(where: { $0.identifier == defaultWebBrowser }) {
openInTitle = option.title
if case let .openUrl(url) = option.action() {
openInUrl = url
} else {
openInUrl = url
}
} else {
openInTitle = "Safari"
openInUrl = url
}
let items: [ContextMenuItem] = [
.custom(fontItem, false),
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceSerif ? emptyIcon : checkIcon, action: { [weak self] (controller, action) in
if let strongSelf = self {
strongSelf.interaction?.updateForceSerif(false)
action(.default)
}
})), .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceSerif ? checkIcon : emptyIcon, action: { [weak self] (controller, action) in
if let strongSelf = self {
strongSelf.interaction?.updateForceSerif(true)
action(.default)
}
})),
.separator,
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
if let strongSelf = self {
strongSelf.interaction?.openSearch()
action(.default)
}
})),
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
if let strongSelf = self {
strongSelf.context.sharedContext.applicationBindings.openUrl(openInUrl)
}
action(.default)
}))]
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))))
strongSelf.present?(controller, nil)
})
}
func scrollToTop() {
self.content?.scrollToTop()
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationHeight)
self.updatePanelsLayout(transition: transition)
}
private func updatePanelsLayout(transition: ContainedViewLayoutTransition) {
guard let (layout, navigationHeight) = self.validLayout else {
return
}
var insets = layout.insets(options: .input)
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
let navigationBarFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: navigationHeight))
transition.updateFrame(node: self.navigationBar, frame: navigationBarFrame)
self.navigationBar.updateLayout(size: navigationBarFrame.size, insets: insets, layoutMetrics: layout.metrics, readingProgress: 0.0, collapseTransition: 0.0, transition: transition)
let toolbarSize = self.toolbar.updateLayout(width: layout.size.width, insets: insets, layoutMetrics: layout.metrics, collapseTransition: 0.0, transition: transition)
let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarSize.height), size: toolbarSize)
transition.updateFrame(node: self.toolbar, frame: toolbarFrame)
let contentFrame = CGRect(origin: CGPoint(), size: layout.size)
transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
if let content = self.content {
content.updateLayout(size: layout.size, insets: insets, transition: transition)
transition.updateFrame(node: content, frame: contentFrame)
}
}
}