NetNewsWire/NetNewsWire/MainWindow/Detail/DetailViewController.swift

258 lines
6.4 KiB
Swift
Raw Normal View History

2017-05-27 19:43:27 +02:00
//
// DetailViewController.swift
2018-08-29 07:18:24 +02:00
// NetNewsWire
2017-05-27 19:43:27 +02:00
//
// Created by Brent Simmons on 7/26/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import WebKit
import RSCore
import Articles
import RSWeb
2017-05-27 19:43:27 +02:00
final class DetailViewController: NSViewController, WKUIDelegate {
@IBOutlet var containerView: DetailContainerView!
@IBOutlet var statusBarView: DetailStatusBarView!
var webview: DetailWebView!
var articles: [Article]? {
didSet {
2019-02-11 07:06:03 +01:00
if articles == oldValue {
return
}
statusBarView.mouseoverLink = nil
reloadHTML()
}
}
private var article: Article? {
return articles?.first
2017-05-27 19:43:27 +02:00
}
2017-12-20 22:39:31 +01:00
private var webviewIsHidden: Bool {
return containerView.contentView !== webview
}
2017-05-27 19:43:27 +02:00
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)
2017-05-27 19:43:27 +02:00
webview.uiDelegate = self
webview.navigationDelegate = self
webview.translatesAutoresizingMaskIntoConstraints = false
if let userAgent = UserAgent.fromInfoPlist() {
webview.customUserAgent = userAgent
}
reloadHTML()
containerView.contentView = webview
containerView.viewController = self
2017-05-27 19:43:27 +02:00
}
private struct MessageName {
static let mouseDidEnter = "mouseDidEnter"
static let mouseDidExit = "mouseDidExit"
}
2019-02-11 07:06:03 +01:00
// MARK: Scrolling
2017-12-20 22:39:31 +01:00
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)
}
2019-02-11 07:06:03 +01:00
// MARK: Notifications
2017-05-27 19:43:27 +02:00
@objc func timelineSelectionDidChange(_ notification: Notification) {
2017-05-27 19:43:27 +02:00
guard let userInfo = notification.userInfo else {
return
}
guard let timelineView = userInfo[UserInfoKey.view] as? NSView, timelineView.window === view.window else {
return
2017-05-27 19:43:27 +02:00
}
let timelineArticles = userInfo[UserInfoKey.articles] as? ArticleArray
articles = timelineArticles
2017-05-27 19:43:27 +02:00
}
}
2019-02-11 07:06:03 +01:00
// MARK: WKNavigationDelegate
extension DetailViewController: WKNavigationDelegate {
2017-05-27 19:43:27 +02:00
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
2017-05-27 19:43:27 +02:00
if navigationAction.navigationType == .linkActivated {
2017-05-27 19:43:27 +02:00
if let url = navigationAction.request.url {
Browser.open(url.absoluteString)
2017-05-27 19:43:27 +02:00
}
2017-05-27 19:43:27 +02:00
decisionHandler(.cancel)
return
}
2017-05-27 19:43:27 +02:00
decisionHandler(.allow)
}
}
2019-02-11 07:06:03 +01:00
// 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
extension DetailViewController: DetailWebViewControllerDelegate {
func mouseDidEnter(_ link: String) {
guard !link.isEmpty else {
return
}
statusBarView.mouseoverLink = link
}
func mouseDidExit(_ link: String) {
statusBarView.mouseoverLink = nil
}
}
2019-02-11 07:06:03 +01:00
// MARK: Private
2017-12-20 22:39:31 +01:00
private extension DetailViewController {
func reloadHTML() {
let html: String
2019-02-11 07:06:03 +01:00
let style = ArticleStylesManager.shared.currentStyle
let appearance = self.view.effectiveAppearance
var baseURL: URL? = nil
2019-02-11 07:06:03 +01:00
if let articles = articles, articles.count > 1 {
html = ArticleRenderer.multipleSelectionHTML(style: style, appearance: appearance)
}
2019-02-11 07:06:03 +01:00
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)
}
2019-02-11 07:06:03 +01:00
webview.loadHTMLString(html, baseURL: baseURL)
}
2017-12-20 22:39:31 +01:00
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()
}
}
2019-02-11 07:06:03 +01:00
// MARK: - ScrollInfo
2017-12-20 22:39:31 +01:00
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
}
}