// // DetailViewController.swift // Evergreen // // Created by Brent Simmons on 7/26/15. // Copyright © 2015 Ranchero Software, LLC. All rights reserved. // import Foundation import WebKit import RSCore import Data import RSWeb final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate { @IBOutlet var containerView: DetailContainerView! var webview: WKWebView! var noSelectionView: NoSelectionView! var article: Article? { didSet { reloadHTML() showOrHideWebView() } } private var webviewIsHidden: Bool { return containerView.contentView !== webview } private struct MessageName { static let mouseDidEnter = "mouseDidEnter" static let mouseDidExit = "mouseDidExit" } override func viewDidLoad() { NotificationCenter.default.addObserver(self, selector: #selector(timelineSelectionDidChange(_:)), name: .TimelineSelectionDidChange, object: nil) let preferences = WKPreferences() preferences.minimumFontSize = 12.0 preferences.javaScriptCanOpenWindowsAutomatically = false preferences.javaEnabled = false preferences.javaScriptEnabled = true preferences.plugInsEnabled = false let configuration = WKWebViewConfiguration() configuration.preferences = preferences let userContentController = WKUserContentController() userContentController.add(self, name: MessageName.mouseDidEnter) userContentController.add(self, name: MessageName.mouseDidExit) configuration.userContentController = userContentController webview = WKWebView(frame: self.view.bounds, configuration: configuration) webview.uiDelegate = self webview.navigationDelegate = self webview.translatesAutoresizingMaskIntoConstraints = false if let userAgent = UserAgent.fromInfoPlist() { webview.customUserAgent = userAgent } noSelectionView = NoSelectionView(frame: self.view.bounds) containerView.viewController = self showOrHideWebView() } // MARK: - Scrolling func canScrollDown(_ callback: @escaping (Bool) -> Void) { if webviewIsHidden { callback(false) return } fetchScrollInfo { (scrollInfo) in callback(scrollInfo?.canScrollDown ?? false) } } override func scrollPageDown(_ sender: Any?) { guard !webviewIsHidden else { return } webview.scrollPageDown(sender) } // MARK: Notifications @objc func timelineSelectionDidChange(_ notification: Notification) { guard let userInfo = notification.userInfo else { return } guard let timelineView = userInfo[UserInfoKey.view] as? NSView, timelineView.window === view.window else { return } let timelineArticle = userInfo[UserInfoKey.article] as? Article article = timelineArticle } func viewWillStartLiveResize() { webview.evaluateJavaScript("document.body.style.overflow = 'hidden';", completionHandler: nil) } func viewDidEndLiveResize() { webview.evaluateJavaScript("document.body.style.overflow = 'visible';", completionHandler: nil) } // MARK: Private private func reloadHTML() { if let article = article { let articleRenderer = ArticleRenderer(article: article, style: ArticleStylesManager.shared.currentStyle) webview.loadHTMLString(articleRenderer.html, baseURL: articleRenderer.baseURL) } else { webview.loadHTMLString("", baseURL: nil) } } private func showOrHideWebView() { if let _ = article { switchToView(webview) } else { switchToView(noSelectionView) } } private func switchToView(_ view: NSView) { if containerView.contentView == view { return } containerView.contentView = view } // MARK: WKNavigationDelegate public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if navigationAction.navigationType == .linkActivated { if let url = navigationAction.request.url { Browser.open(url.absoluteString) } decisionHandler(.cancel) return } decisionHandler(.allow) } } extension DetailViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if message.name == MessageName.mouseDidEnter, let link = message.body as? String { mouseDidEnter(link) } else if message.name == MessageName.mouseDidExit, let link = message.body as? String{ mouseDidExit(link) } } private func mouseDidEnter(_ link: String) { if link.isEmpty { return } var userInfo = UserInfoDictionary() userInfo[UserInfoKey.view] = view userInfo[UserInfoKey.url] = link NotificationCenter.default.post(name: .MouseDidEnterLink, object: self, userInfo: userInfo) } private func mouseDidExit(_ link: String) { var userInfo = UserInfoDictionary() userInfo[UserInfoKey.view] = view userInfo[UserInfoKey.url] = link NotificationCenter.default.post(name: .MouseDidExitLink, object: self, userInfo: userInfo) } } private extension DetailViewController { func fetchScrollInfo(_ callback: @escaping (ScrollInfo?) -> Void) { let javascriptString = "var x = {contentHeight: document.body.scrollHeight, offsetY: document.body.scrollTop}; x" webview.evaluateJavaScript(javascriptString) { (info, error) in guard let info = info as? [String: Any] else { callback(nil) return } guard let contentHeight = info["contentHeight"] as? CGFloat, let offsetY = info["offsetY"] as? CGFloat else { callback(nil) return } let scrollInfo = ScrollInfo(contentHeight: contentHeight, viewHeight: self.webview.frame.height, offsetY: offsetY) callback(scrollInfo) } } } final class DetailContainerView: NSView { @IBOutlet var detailStatusBarView: DetailStatusBarView! weak var viewController: DetailViewController? = nil private var didConfigureLayer = false override var wantsUpdateLayer: Bool { return true } var contentView: NSView? { didSet { if let oldContentView = oldValue { oldContentView.removeFromSuperviewWithoutNeedingDisplay() } if let contentView = contentView { contentView.translatesAutoresizingMaskIntoConstraints = false addSubview(contentView, positioned: .below, relativeTo: detailStatusBarView) rs_addFullSizeConstraints(forSubview: contentView) } } } override func viewWillStartLiveResize() { viewController?.viewWillStartLiveResize() } override func viewDidEndLiveResize() { viewController?.viewDidEndLiveResize() } override func updateLayer() { guard !didConfigureLayer else { return } if let layer = layer { let color = appDelegate.currentTheme.color(forKey: "MainWindow.Detail.backgroundColor") layer.backgroundColor = color.cgColor didConfigureLayer = true } } } final class NoSelectionView: NSView { private var didConfigureLayer = false override var wantsUpdateLayer: Bool { return true } override func updateLayer() { guard !didConfigureLayer else { return } if let layer = layer { let color = appDelegate.currentTheme.color(forKey: "MainWindow.Detail.noSelectionView.backgroundColor") layer.backgroundColor = color.cgColor didConfigureLayer = true } } } private struct ScrollInfo { let contentHeight: CGFloat let viewHeight: CGFloat let offsetY: CGFloat let canScrollDown: Bool let canScrollUp: Bool init(contentHeight: CGFloat, viewHeight: CGFloat, offsetY: CGFloat) { self.contentHeight = contentHeight self.viewHeight = viewHeight self.offsetY = offsetY self.canScrollDown = viewHeight + offsetY < contentHeight self.canScrollUp = offsetY > 0.1 } }