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
|
2018-07-24 03:29:08 +02:00
|
|
|
import Articles
|
2017-12-03 21:13:44 +01:00
|
|
|
import RSWeb
|
2017-05-27 19:43:27 +02:00
|
|
|
|
2018-02-19 06:49:46 +01:00
|
|
|
final class DetailViewController: NSViewController, WKUIDelegate {
|
2017-11-06 05:31:50 +01:00
|
|
|
|
2017-12-16 19:18:02 +01:00
|
|
|
@IBOutlet var containerView: DetailContainerView!
|
2019-02-11 06:39:11 +01:00
|
|
|
@IBOutlet var statusBarView: DetailStatusBarView!
|
|
|
|
|
2018-02-10 20:16:09 +01:00
|
|
|
var webview: DetailWebView!
|
2017-12-03 21:38:44 +01:00
|
|
|
|
2018-02-19 06:49:46 +01:00
|
|
|
var articles: [Article]? {
|
|
|
|
didSet {
|
2019-02-11 07:06:03 +01:00
|
|
|
if articles == oldValue {
|
2018-02-19 06:49:46 +01:00
|
|
|
return
|
|
|
|
}
|
2019-02-11 06:39:11 +01:00
|
|
|
statusBarView.mouseoverLink = nil
|
2018-09-15 03:00:51 +02:00
|
|
|
reloadHTML()
|
2018-02-19 06:49:46 +01:00
|
|
|
}
|
|
|
|
}
|
2019-02-11 06:46:53 +01:00
|
|
|
|
2018-02-19 06:49:46 +01:00
|
|
|
private var article: Article? {
|
2019-02-11 06:46:53 +01:00
|
|
|
return articles?.first
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
2017-11-06 05:31:50 +01: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
|
|
|
|
|
2017-11-06 05:31:50 +01:00
|
|
|
let userContentController = WKUserContentController()
|
|
|
|
userContentController.add(self, name: MessageName.mouseDidEnter)
|
|
|
|
userContentController.add(self, name: MessageName.mouseDidExit)
|
|
|
|
configuration.userContentController = userContentController
|
|
|
|
|
2018-02-10 20:16:09 +01:00
|
|
|
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
|
2017-12-03 21:13:44 +01:00
|
|
|
if let userAgent = UserAgent.fromInfoPlist() {
|
|
|
|
webview.customUserAgent = userAgent
|
|
|
|
}
|
|
|
|
|
2018-09-15 03:00:51 +02:00
|
|
|
reloadHTML()
|
|
|
|
containerView.contentView = webview
|
2017-12-16 19:18:02 +01:00
|
|
|
containerView.viewController = self
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
|
|
|
|
2019-02-12 07:36:31 +01: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
|
|
|
|
2017-12-18 21:34:07 +01:00
|
|
|
@objc func timelineSelectionDidChange(_ notification: Notification) {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
2017-12-18 21:34:07 +01: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
|
|
|
}
|
2017-12-18 21:34:07 +01:00
|
|
|
|
2018-02-19 06:49:46 +01:00
|
|
|
let timelineArticles = userInfo[UserInfoKey.articles] as? ArticleArray
|
|
|
|
articles = timelineArticles
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
2018-02-19 06:49:46 +01:00
|
|
|
}
|
2017-12-03 21:38:44 +01:00
|
|
|
|
2019-02-11 07:06:03 +01:00
|
|
|
// MARK: WKNavigationDelegate
|
2017-12-03 21:38:44 +01:00
|
|
|
|
2018-02-19 06:49:46 +01:00
|
|
|
extension DetailViewController: WKNavigationDelegate {
|
2017-12-03 21:38:44 +01:00
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
2018-02-19 06:49:46 +01:00
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
if navigationAction.navigationType == .linkActivated {
|
2018-02-19 06:49:46 +01:00
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
if let url = navigationAction.request.url {
|
2017-10-06 03:12:58 +02:00
|
|
|
Browser.open(url.absoluteString)
|
2017-05-27 19:43:27 +02:00
|
|
|
}
|
2018-02-19 06:49:46 +01:00
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
decisionHandler(.cancel)
|
|
|
|
return
|
|
|
|
}
|
2018-02-19 06:49:46 +01:00
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
decisionHandler(.allow)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-11 07:06:03 +01:00
|
|
|
// MARK: WKScriptMessageHandler
|
2018-02-19 06:49:46 +01:00
|
|
|
|
2017-11-06 05:31:50 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2019-02-13 03:12:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: DetailWebViewControllerDelegate
|
|
|
|
|
|
|
|
extension DetailViewController: DetailWebViewControllerDelegate {
|
2017-11-06 05:31:50 +01:00
|
|
|
|
2019-02-13 03:12:12 +01:00
|
|
|
func mouseDidEnter(_ link: String) {
|
2019-02-11 06:39:11 +01:00
|
|
|
guard !link.isEmpty else {
|
2017-11-06 05:31:50 +01:00
|
|
|
return
|
|
|
|
}
|
2019-02-11 06:39:11 +01:00
|
|
|
statusBarView.mouseoverLink = link
|
2017-11-06 05:31:50 +01:00
|
|
|
}
|
|
|
|
|
2019-02-13 03:12:12 +01:00
|
|
|
func mouseDidExit(_ link: String) {
|
2019-02-11 06:39:11 +01:00
|
|
|
statusBarView.mouseoverLink = nil
|
2017-11-06 05:31:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-11 07:06:03 +01:00
|
|
|
// MARK: Private
|
2018-02-19 06:49:46 +01:00
|
|
|
|
2017-12-20 22:39:31 +01:00
|
|
|
private extension DetailViewController {
|
|
|
|
|
2018-02-19 06:49:46 +01:00
|
|
|
func reloadHTML() {
|
2019-02-12 07:36:31 +01:00
|
|
|
let html: String
|
2019-02-11 07:06:03 +01:00
|
|
|
let style = ArticleStylesManager.shared.currentStyle
|
|
|
|
let appearance = self.view.effectiveAppearance
|
2019-02-12 07:36:31 +01:00
|
|
|
var baseURL: URL? = nil
|
2018-02-19 06:49:46 +01:00
|
|
|
|
2019-02-11 07:06:03 +01:00
|
|
|
if let articles = articles, articles.count > 1 {
|
2019-02-12 07:36:31 +01:00
|
|
|
html = ArticleRenderer.multipleSelectionHTML(style: style, appearance: appearance)
|
2018-02-19 06:49:46 +01:00
|
|
|
}
|
2019-02-11 07:06:03 +01:00
|
|
|
else if let article = article {
|
2019-02-12 07:36:31 +01:00
|
|
|
html = ArticleRenderer.articleHTML(article: article, style: style, appearance: appearance)
|
|
|
|
baseURL = ArticleRenderer.baseURL(for: article)
|
2018-02-19 06:49:46 +01:00
|
|
|
}
|
|
|
|
else {
|
2019-02-12 07:36:31 +01:00
|
|
|
html = ArticleRenderer.noSelectionHTML(style: style, appearance: appearance)
|
2018-02-19 06:49:46 +01:00
|
|
|
}
|
2019-02-11 07:06:03 +01:00
|
|
|
|
2019-02-12 07:36:31 +01:00
|
|
|
webview.loadHTMLString(html, baseURL: baseURL)
|
2018-02-19 06:49:46 +01:00
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-11 05:51:28 +01:00
|
|
|
// MARK: - DetailContainerView
|
2018-02-19 06:49:46 +01:00
|
|
|
|
2017-12-16 19:18:02 +01:00
|
|
|
final class DetailContainerView: NSView {
|
|
|
|
|
2017-12-29 03:41:01 +01:00
|
|
|
@IBOutlet var detailStatusBarView: DetailStatusBarView!
|
|
|
|
|
2017-12-16 19:18:02 +01:00
|
|
|
weak var viewController: DetailViewController? = nil
|
|
|
|
|
2019-02-13 03:24:46 +01:00
|
|
|
override var isOpaque: Bool {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2017-12-16 19:18:02 +01:00
|
|
|
var contentView: NSView? {
|
|
|
|
didSet {
|
2019-02-11 05:51:28 +01:00
|
|
|
if contentView == oldValue {
|
|
|
|
return
|
2017-12-16 19:18:02 +01:00
|
|
|
}
|
2019-02-11 05:51:28 +01:00
|
|
|
oldValue?.removeFromSuperviewWithoutNeedingDisplay()
|
2017-12-16 19:18:02 +01:00
|
|
|
if let contentView = contentView {
|
|
|
|
contentView.translatesAutoresizingMaskIntoConstraints = false
|
2017-12-29 03:41:01 +01:00
|
|
|
addSubview(contentView, positioned: .below, relativeTo: detailStatusBarView)
|
2017-12-16 19:18:02 +01:00
|
|
|
rs_addFullSizeConstraints(forSubview: contentView)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-22 04:55:07 +02:00
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
|
|
NSColor.textBackgroundColor.setFill()
|
|
|
|
dirtyRect.fill()
|
2017-12-16 19:18:02 +01:00
|
|
|
}
|
2017-12-03 21:45:34 +01:00
|
|
|
}
|
|
|
|
|
2019-02-11 07:06:03 +01:00
|
|
|
// MARK: - ScrollInfo
|
2018-02-19 06:49:46 +01:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|