mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
473 lines
20 KiB
Swift
473 lines
20 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import TelegramCore
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import PresentationDataUtils
|
|
import AccountContext
|
|
import WebKit
|
|
import AppBundle
|
|
import PromptUI
|
|
import SafariServices
|
|
import ShareController
|
|
import UndoUI
|
|
import UrlEscaping
|
|
import PDFKit
|
|
|
|
final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
|
|
private let context: AccountContext
|
|
private var presentationData: PresentationData
|
|
|
|
private let webView: PDFView
|
|
private let scrollView: UIScrollView!
|
|
|
|
let uuid: UUID
|
|
|
|
private var _state: BrowserContentState
|
|
private let statePromise: Promise<BrowserContentState>
|
|
|
|
var currentState: BrowserContentState {
|
|
return self._state
|
|
}
|
|
var state: Signal<BrowserContentState, NoError> {
|
|
return self.statePromise.get()
|
|
}
|
|
|
|
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
|
|
var openAppUrl: (String) -> Void = { _ in }
|
|
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
|
|
var minimize: () -> Void = { }
|
|
var close: () -> Void = { }
|
|
var present: (ViewController, Any?) -> Void = { _, _ in }
|
|
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
|
|
var getNavigationController: () -> NavigationController? = { return nil }
|
|
|
|
private var tempFile: TempBoxFile?
|
|
|
|
init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) {
|
|
self.context = context
|
|
self.uuid = UUID()
|
|
self.presentationData = presentationData
|
|
|
|
self.webView = PDFView()
|
|
self.webView.maxScaleFactor = 4.0;
|
|
self.webView.minScaleFactor = self.webView.scaleFactorForSizeToFit
|
|
self.webView.autoScales = true
|
|
|
|
var scrollView: UIScrollView?
|
|
for view in self.webView.subviews {
|
|
if let view = view as? UIScrollView {
|
|
scrollView = view
|
|
} else {
|
|
for subview in view.subviews {
|
|
if let subview = subview as? UIScrollView {
|
|
scrollView = subview
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.scrollView = scrollView
|
|
|
|
var title: String = "file"
|
|
if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) {
|
|
// var updatedPath = path
|
|
// if let fileName = file.fileName {
|
|
// let tempFile = TempBox.shared.file(path: path, fileName: fileName)
|
|
// updatedPath = tempFile.path
|
|
// self.tempFile = tempFile
|
|
// title = fileName
|
|
// }
|
|
|
|
self.webView.document = PDFDocument(data: data)
|
|
title = file.fileName ?? "file"
|
|
}
|
|
|
|
self._state = BrowserContentState(title: title, url: "", estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document)
|
|
self.statePromise = Promise<BrowserContentState>(self._state)
|
|
|
|
super.init(frame: .zero)
|
|
|
|
if #available(iOS 15.0, *) {
|
|
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
|
}
|
|
self.addSubview(self.webView)
|
|
|
|
Queue.mainQueue().after(1.0) {
|
|
scrollView?.delegate = self
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func updatePresentationData(_ presentationData: PresentationData) {
|
|
self.presentationData = presentationData
|
|
if #available(iOS 15.0, *) {
|
|
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
|
}
|
|
if let (size, insets, fullInsets) = self.validLayout {
|
|
self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false)
|
|
func updateFontState(_ state: BrowserPresentationState.FontState) {
|
|
self.updateFontState(state, force: false)
|
|
}
|
|
func updateFontState(_ state: BrowserPresentationState.FontState, force: Bool) {
|
|
self.currentFontState = state
|
|
|
|
// let fontFamily = state.isSerif ? "'Georgia, serif'" : "null"
|
|
// let textSizeAdjust = state.size != 100 ? "'\(state.size)%'" : "null"
|
|
// let js = "\(setupFontFunctions) setTelegramFontOverrides(\(fontFamily), \(textSizeAdjust))";
|
|
// self.webView.evaluateJavaScript(js) { _, _ in }
|
|
}
|
|
|
|
private var didSetupSearch = false
|
|
private func setupSearch(completion: @escaping () -> Void) {
|
|
// guard !self.didSetupSearch else {
|
|
// completion()
|
|
// return
|
|
// }
|
|
//
|
|
// let bundle = getAppBundle()
|
|
// guard let scriptPath = bundle.path(forResource: "UIWebViewSearch", ofType: "js") else {
|
|
// return
|
|
// }
|
|
// guard let scriptData = try? Data(contentsOf: URL(fileURLWithPath: scriptPath)) else {
|
|
// return
|
|
// }
|
|
// guard let script = String(data: scriptData, encoding: .utf8) else {
|
|
// return
|
|
// }
|
|
// self.didSetupSearch = true
|
|
// self.webView.evaluateJavaScript(script, completionHandler: { _, error in
|
|
// if error != nil {
|
|
// print()
|
|
// }
|
|
// completion()
|
|
// })
|
|
}
|
|
|
|
private var previousQuery: String?
|
|
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
|
|
// guard self.previousQuery != query else {
|
|
// return
|
|
// }
|
|
// self.previousQuery = query
|
|
// self.setupSearch { [weak self] in
|
|
// if let query = query {
|
|
// let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')"
|
|
// self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in
|
|
// let js = "uiWebview_SearchResultCount"
|
|
// self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in
|
|
// if let result = result as? NSNumber {
|
|
// self?.searchResultsCount = result.intValue
|
|
// completion?(result.intValue)
|
|
// } else {
|
|
// completion?(0)
|
|
// }
|
|
// })
|
|
// })
|
|
// } else {
|
|
// let js = "uiWebview_RemoveAllHighlights()"
|
|
// self?.webView.evaluateJavaScript(js, completionHandler: nil)
|
|
//
|
|
// self?.currentSearchResult = 0
|
|
// self?.searchResultsCount = 0
|
|
// }
|
|
// }
|
|
}
|
|
|
|
private var currentSearchResult: Int = 0
|
|
private var searchResultsCount: Int = 0
|
|
|
|
func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) {
|
|
// let searchResultsCount = self.searchResultsCount
|
|
// var index = self.currentSearchResult - 1
|
|
// if index < 0 {
|
|
// index = searchResultsCount - 1
|
|
// }
|
|
// self.currentSearchResult = index
|
|
//
|
|
// let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
|
// self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
|
// completion?(index, searchResultsCount)
|
|
// })
|
|
}
|
|
|
|
func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) {
|
|
// let searchResultsCount = self.searchResultsCount
|
|
// var index = self.currentSearchResult + 1
|
|
// if index >= searchResultsCount {
|
|
// index = 0
|
|
// }
|
|
// self.currentSearchResult = index
|
|
//
|
|
// let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
|
// self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
|
// completion?(index, searchResultsCount)
|
|
// })
|
|
}
|
|
|
|
func stop() {
|
|
// self.webView.stopLoading()
|
|
}
|
|
|
|
func reload() {
|
|
// self.webView.reload()
|
|
}
|
|
|
|
func navigateBack() {
|
|
// self.webView.goBack()
|
|
}
|
|
|
|
func navigateForward() {
|
|
// self.webView.goForward()
|
|
}
|
|
|
|
func navigateTo(historyItem: BrowserContentState.HistoryItem) {
|
|
// if let webItem = historyItem.webItem {
|
|
// self.webView.go(to: webItem)
|
|
// }
|
|
}
|
|
|
|
func navigateTo(address: String) {
|
|
// let finalUrl = explicitUrl(address)
|
|
// guard let url = URL(string: finalUrl) else {
|
|
// return
|
|
// }
|
|
// self.webView.load(URLRequest(url: url))
|
|
}
|
|
|
|
func scrollToTop() {
|
|
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true)
|
|
}
|
|
|
|
private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)?
|
|
func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) {
|
|
self.validLayout = (size, insets, fullInsets)
|
|
|
|
self.previousScrollingOffset = ScrollingOffsetState(value: self.scrollView.contentOffset.y, isDraggingOrDecelerating: self.scrollView.isDragging || self.scrollView.isDecelerating)
|
|
|
|
let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - insets.bottom))
|
|
var refresh = false
|
|
if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width {
|
|
refresh = true
|
|
}
|
|
transition.setFrame(view: self.webView, frame: webViewFrame)
|
|
|
|
if refresh {
|
|
self.webView.reloadInputViews()
|
|
}
|
|
|
|
// if let error = self.currentError {
|
|
// let errorSize = self.errorView.update(
|
|
// transition: .immediate,
|
|
// component: AnyComponent(
|
|
// ErrorComponent(
|
|
// theme: self.presentationData.theme,
|
|
// title: self.presentationData.strings.Browser_ErrorTitle,
|
|
// text: error.localizedDescription
|
|
// )
|
|
// ),
|
|
// environment: {},
|
|
// containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height)
|
|
// )
|
|
// if self.errorView.superview == nil {
|
|
// self.addSubview(self.errorView)
|
|
// self.errorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
// }
|
|
// self.errorView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - errorSize.width) / 2.0), y: insets.top + floorToScreenPixels((size.height - insets.top - insets.bottom - errorSize.height) / 2.0)), size: errorSize)
|
|
// } else if self.errorView.superview != nil {
|
|
// self.errorView.removeFromSuperview()
|
|
// }
|
|
}
|
|
|
|
private func updateState(_ f: (BrowserContentState) -> BrowserContentState) {
|
|
let updated = f(self._state)
|
|
self._state = updated
|
|
self.statePromise.set(.single(self._state))
|
|
}
|
|
|
|
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 = ComponentTransition(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: ComponentTransition) {
|
|
guard let scrollView = self.scrollView else {
|
|
return
|
|
}
|
|
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)
|
|
|
|
var readingProgress: CGFloat = 0.0
|
|
if !scrollView.contentSize.height.isZero {
|
|
let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top)
|
|
readingProgress = max(0.0, min(1.0, value))
|
|
}
|
|
self.updateState {
|
|
$0.withUpdatedReadingProgress(readingProgress)
|
|
}
|
|
}
|
|
|
|
func resetScrolling() {
|
|
self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4))
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
|
// self.currentError = nil
|
|
self.updateFontState(self.currentFontState, force: true)
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
self.updateState {
|
|
$0
|
|
.withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) })
|
|
.withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) })
|
|
}
|
|
}
|
|
|
|
// func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
|
// if (error as NSError).code != -999 {
|
|
// self.currentError = error
|
|
// } else {
|
|
// self.currentError = nil
|
|
// }
|
|
// if let (size, insets) = self.validLayout {
|
|
// self.updateLayout(size: size, insets: insets, transition: .immediate)
|
|
// }
|
|
// }
|
|
//
|
|
// func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
|
// if (error as NSError).code != -999 {
|
|
// self.currentError = error
|
|
// } else {
|
|
// self.currentError = nil
|
|
// }
|
|
// if let (size, insets) = self.validLayout {
|
|
// self.updateLayout(size: size, insets: insets, transition: .immediate)
|
|
// }
|
|
// }
|
|
|
|
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
if navigationAction.targetFrame == nil {
|
|
if let url = navigationAction.request.url?.absoluteString {
|
|
self.open(url: url, new: true)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func webViewDidClose(_ webView: WKWebView) {
|
|
self.close()
|
|
}
|
|
|
|
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
|
|
func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
|
|
decisionHandler(.prompt)
|
|
}
|
|
|
|
|
|
// @available(iOS 13.0, *)
|
|
// func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) {
|
|
// guard let url = elementInfo.linkURL else {
|
|
// completionHandler(nil)
|
|
// return
|
|
// }
|
|
// let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
// let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
|
|
// return UIMenu(title: "", children: [
|
|
// UIAction(title: presentationData.strings.Browser_ContextMenu_Open, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
|
// self?.open(url: url.absoluteString, new: false)
|
|
// }),
|
|
// UIAction(title: presentationData.strings.Browser_ContextMenu_OpenInNewTab, image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
|
// self?.open(url: url.absoluteString, new: true)
|
|
// }),
|
|
// UIAction(title: presentationData.strings.Browser_ContextMenu_AddToReadingList, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in
|
|
// let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil)
|
|
// }),
|
|
// UIAction(title: presentationData.strings.Browser_ContextMenu_CopyLink, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
|
// UIPasteboard.general.string = url.absoluteString
|
|
// self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
|
// }),
|
|
// UIAction(title: presentationData.strings.Browser_ContextMenu_Share, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
|
// self?.share(url: url.absoluteString)
|
|
// })
|
|
// ])
|
|
// }
|
|
// completionHandler(configuration)
|
|
// }
|
|
|
|
private func open(url: String, new: Bool) {
|
|
let subject: BrowserScreen.Subject = .webPage(url: url)
|
|
if new, let navigationController = self.getNavigationController() {
|
|
navigationController._keepModalDismissProgress = true
|
|
self.minimize()
|
|
let controller = BrowserScreen(context: self.context, subject: subject)
|
|
navigationController._keepModalDismissProgress = true
|
|
navigationController.pushViewController(controller)
|
|
} else {
|
|
self.pushContent(subject)
|
|
}
|
|
}
|
|
|
|
private func share(url: String) {
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let shareController = ShareController(context: self.context, subject: .url(url))
|
|
shareController.actionCompleted = { [weak self] in
|
|
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
|
}
|
|
self.present(shareController, nil)
|
|
}
|
|
|
|
func addToRecentlyVisited() {
|
|
}
|
|
|
|
func makeContentSnapshotView() -> UIView? {
|
|
return nil
|
|
}
|
|
}
|