diff --git a/NetNewsWire/MainWindow/Detail/DetailViewController.swift b/NetNewsWire/MainWindow/Detail/DetailViewController.swift index 0164ffe6b..15004588c 100644 --- a/NetNewsWire/MainWindow/Detail/DetailViewController.swift +++ b/NetNewsWire/MainWindow/Detail/DetailViewController.swift @@ -12,140 +12,60 @@ import RSCore import Articles import RSWeb +enum DetailState: Equatable { + case noSelection + case multipleSelection + case article(Article) +} + final class DetailViewController: NSViewController, WKUIDelegate { @IBOutlet var containerView: DetailContainerView! @IBOutlet var statusBarView: DetailStatusBarView! - - var webview: DetailWebView! - var articles: [Article]? { + enum WebViewType { + case regular, search + } + + lazy var regularWebViewController = { + return createWebViewController() + }() + + lazy var searchWebViewController = { + return createWebViewController() + }() + + var currentWebViewController: DetailWebViewController! { didSet { - if articles == oldValue { + let webview = currentWebViewController.view + if containerView.contentView === webview { return } statusBarView.mouseoverLink = nil - reloadHTML() + containerView.contentView = webview } } - private var article: Article? { - return articles?.first - } - - private var webviewIsHidden: Bool { - return containerView.contentView !== webview - } - 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 = DetailWebView(frame: self.view.bounds, configuration: configuration) - webview.uiDelegate = self - webview.navigationDelegate = self - webview.translatesAutoresizingMaskIntoConstraints = false - if let userAgent = UserAgent.fromInfoPlist() { - webview.customUserAgent = userAgent - } - - reloadHTML() - containerView.contentView = webview - containerView.viewController = self + currentWebViewController = regularWebViewController } - private struct MessageName { - static let mouseDidEnter = "mouseDidEnter" - static let mouseDidExit = "mouseDidExit" - } + // MARK: - API - // MARK: Scrolling + func showState(_ state: DetailState, in webViewType: WebViewType) { + // TODO: also to-do is caller + } func canScrollDown(_ callback: @escaping (Bool) -> Void) { - if webviewIsHidden { - callback(false) - return - } - - fetchScrollInfo { (scrollInfo) in - callback(scrollInfo?.canScrollDown ?? false) - } + currentWebViewController.canScrollDown(callback) } 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 timelineArticles = userInfo[UserInfoKey.articles] as? ArticleArray - articles = timelineArticles + currentWebViewController.scrollPageDown(sender) } } -// MARK: WKNavigationDelegate - -extension DetailViewController: 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) - } -} - -// MARK: WKScriptMessageHandler - -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) - } - } -} - -// MARK: DetailWebViewControllerDelegate +// MARK: - DetailWebViewControllerDelegate extension DetailViewController: DetailWebViewControllerDelegate { @@ -161,97 +81,14 @@ extension DetailViewController: DetailWebViewControllerDelegate { } } -// MARK: Private +// MARK: - Private private extension DetailViewController { - func reloadHTML() { - let html: String - let style = ArticleStylesManager.shared.currentStyle - let appearance = self.view.effectiveAppearance - var baseURL: URL? = nil - - if let articles = articles, articles.count > 1 { - html = ArticleRenderer.multipleSelectionHTML(style: style, appearance: appearance) - } - else if let article = article { - html = ArticleRenderer.articleHTML(article: article, style: style, appearance: appearance) - baseURL = ArticleRenderer.baseURL(for: article) - } - else { - html = ArticleRenderer.noSelectionHTML(style: style, appearance: appearance) - } - - webview.loadHTMLString(html, baseURL: baseURL) - } - - 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) - } - } -} - -// MARK: - DetailContainerView - -final class DetailContainerView: NSView { - - @IBOutlet var detailStatusBarView: DetailStatusBarView! - - weak var viewController: DetailViewController? = nil - - override var isOpaque: Bool { - return true - } - - var contentView: NSView? { - didSet { - if contentView == oldValue { - return - } - oldValue?.removeFromSuperviewWithoutNeedingDisplay() - if let contentView = contentView { - contentView.translatesAutoresizingMaskIntoConstraints = false - addSubview(contentView, positioned: .below, relativeTo: detailStatusBarView) - rs_addFullSizeConstraints(forSubview: contentView) - } - } - } - - override func draw(_ dirtyRect: NSRect) { - NSColor.textBackgroundColor.setFill() - dirtyRect.fill() - } -} - -// MARK: - ScrollInfo - -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 + func createWebViewController() -> DetailWebViewController { + let controller = DetailWebViewController() + controller.delegate = self + controller.state = .noSelection + return controller } } diff --git a/NetNewsWire/MainWindow/Detail/DetailWebViewController.swift b/NetNewsWire/MainWindow/Detail/DetailWebViewController.swift index 721085ca0..df77cfafa 100644 --- a/NetNewsWire/MainWindow/Detail/DetailWebViewController.swift +++ b/NetNewsWire/MainWindow/Detail/DetailWebViewController.swift @@ -11,12 +11,6 @@ import WebKit import RSWeb import Articles -enum DetailWebViewState: Equatable { - case noSelection - case multipleSelection - case article(Article) -} - protocol DetailWebViewControllerDelegate: class { func mouseDidEnter(_ link: String) func mouseDidExit(_ link: String) @@ -26,7 +20,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { weak var delegate: DetailWebViewControllerDelegate? var webview: DetailWebView! - var state: DetailWebViewState = .noSelection { + var state: DetailState = .noSelection { didSet { if state != oldValue { reloadHTML() @@ -34,6 +28,11 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { } } + private struct MessageName { + static let mouseDidEnter = "mouseDidEnter" + static let mouseDidExit = "mouseDidExit" + } + override func loadView() { let preferences = WKPreferences() preferences.minimumFontSize = 12.0 @@ -57,12 +56,26 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { if let userAgent = UserAgent.fromInfoPlist() { webview.customUserAgent = userAgent } + view = webview + + DispatchQueue.main.async { + // Must do this async, because reloadHTML references view.effectiveAppearance, + // which causes loadView to get called. Infinite loop. + self.reloadHTML() + } } - private struct MessageName { - static let mouseDidEnter = "mouseDidEnter" - static let mouseDidExit = "mouseDidExit" + // MARK: Scrolling + + func canScrollDown(_ callback: @escaping (Bool) -> Void) { + fetchScrollInfo { (scrollInfo) in + callback(scrollInfo?.canScrollDown ?? false) + } + } + + override func scrollPageDown(_ sender: Any?) { + webview.scrollPageDown(sender) } } @@ -103,7 +116,7 @@ private extension DetailWebViewController { func reloadHTML() { let style = ArticleStylesManager.shared.currentStyle - let appearance = self.view.effectiveAppearance + let appearance = view.effectiveAppearance let html: String var baseURL: URL? = nil @@ -119,8 +132,28 @@ private extension DetailWebViewController { webview.loadHTMLString(html, baseURL: baseURL) } + + 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) + } + } } +// MARK: - Article extension + private extension Article { var baseURL: URL? { @@ -148,3 +181,24 @@ private extension Article { return url } } + + +// MARK: - ScrollInfo + +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 + } +}