diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index 5130baaa2..0a8e1e5f0 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -5,7 +5,40 @@ https://ranchero.com/downloads/evergreen-beta.xml Most recent Evergreen changes with links to updates. en - + + + Version 1.0d23 + Decorate the tree!

+ ]]>
+ Tue, 5 Dec 2017 13:00:00 -0800 + + 10.13 +
+ + + Version 1.0d22 + Refresh all after importing OPML.

+

Fetch unread counts from database at startup.

+

Make resizing the timeline view marginally faster.

+

Make the favicon downloading system use a little less memory.

+

Read a newly-added feed immediately, instead of waiting for the next refresh-all.

+

Use 38-pt-wide toolbar icons, a la Mail.

+

Parse RSS 1.0 (RDF) feeds. Pinboard uses these (I couldn’t find them anywhere else).

+

Make the window title-less. You can bring back the title using a hidden pref.

+

Save feed authors.

+

Save article authors.

+

Use updated @2x next-unread icon from Brad.

+

Fix bug detecting feed type for Dr. Drang’s JSON Feed.

+

Fix bug detecting Macworld’s RSS feed as an RSS feed. The feed doesn’t start with the standard XML header.

+

Set user-agent on the detail view’s webview.

+ ]]>
+ Mon, 4 Dec 2017 13:00:00 -0800 + + 10.13 +
+ Version 1.0d20 CFBundlePackageType APPL CFBundleShortVersionString - 1.0d21 + 1.0d23 CFBundleVersion - 513 + 515 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright diff --git a/Evergreen/MainWindow/Detail/DetailViewController.swift b/Evergreen/MainWindow/Detail/DetailViewController.swift index 8cd070f39..1d17a76f9 100644 --- a/Evergreen/MainWindow/Detail/DetailViewController.swift +++ b/Evergreen/MainWindow/Detail/DetailViewController.swift @@ -10,14 +10,17 @@ import Foundation import WebKit import RSCore import Data +import RSWeb -class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate { +final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate { var webview: WKWebView! - + var noSelectionView: NoSelectionView! + var article: Article? { didSet { reloadHTML() + showOrHideWebView() } } @@ -49,11 +52,16 @@ class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate webview.uiDelegate = self webview.navigationDelegate = self webview.translatesAutoresizingMaskIntoConstraints = false + if let userAgent = UserAgent.fromInfoPlist() { + webview.customUserAgent = userAgent + } + + noSelectionView = NoSelectionView(frame: self.view.bounds) + let boxView = self.view as! DetailBox - boxView.contentView = webview - boxView.rs_addFullSizeConstraints(forSubview: webview) - boxView.viewController = self + + showOrHideWebView() } // MARK: Notifications @@ -88,7 +96,27 @@ class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate webview.loadHTMLString("", baseURL: nil) } } - + + private func showOrHideWebView() { + + if let _ = article { + switchToView(webview) + } + else { + switchToView(noSelectionView) + } + } + + private func switchToView(_ view: NSView) { + + let boxView = self.view as! DetailBox + if boxView.contentView == view { + return + } + boxView.contentView = view + boxView.rs_addFullSizeConstraints(forSubview: view) + } + // MARK: WKNavigationDelegate public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { @@ -142,7 +170,7 @@ extension DetailViewController: WKScriptMessageHandler { } } -class DetailBox: NSBox { +final class DetailBox: NSBox { weak var viewController: DetailViewController? @@ -156,3 +184,25 @@ class DetailBox: NSBox { viewController?.viewDidEndLiveResize() } } + +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 + } + } +} + diff --git a/Evergreen/Resources/DB5.plist b/Evergreen/Resources/DB5.plist index 93b092184..fd88a1b7e 100644 --- a/Evergreen/Resources/DB5.plist +++ b/Evergreen/Resources/DB5.plist @@ -108,6 +108,14 @@ 4 + Detail + + noSelectionView + + backgroundColor + FFFFFF + + diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 0a8cbc55d..5243ba209 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -140,6 +140,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, DispatchQueue.main.async { self.updateUnreadCount() + self.fetchAllUnreadCounts() } } @@ -302,6 +303,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } importOPMLItems(children, parentFolder: nil) dirty = true + + DispatchQueue.main.async { + self.refreshAll() + } } public func updateUnreadCounts(for feeds: Set) { @@ -622,6 +627,28 @@ private extension Account { NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: statuses, UserInfoKey.articles: articles, UserInfoKey.feeds: feeds]) } + + func fetchAllUnreadCounts() { + + database.fetchAllNonZeroUnreadCounts { (unreadCountDictionary) in + + if unreadCountDictionary.isEmpty { + return + } + + self.flattenedFeeds().forEach{ (feed) in + + // When the unread count is zero, it won’t appear in unreadCountDictionary. + + if let unreadCount = unreadCountDictionary[feed] { + feed.unreadCount = unreadCount + } + else { + feed.unreadCount = 0 + } + } + } + } } // MARK: - Container Overrides diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index 89708a4a7..584b3a695 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -174,6 +174,37 @@ final class ArticlesTable: DatabaseTable { } } + func fetchAllUnreadCounts(_ completion: @escaping UnreadCountCompletionBlock) { + + // Returns only where unreadCount > 0. + + let cutoffDate = articleCutoffDate + + queue.fetch { (database) in + + let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 and userDeleted=0 and (starred=1 or dateArrived>?) group by feedID;" + + guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else { + DispatchQueue.main.async() { + completion(UnreadCountDictionary()) + } + return + } + + var d = UnreadCountDictionary() + while resultSet.next() { + let unreadCount = resultSet.long(forColumnIndex: 1) + if let feedID = resultSet.string(forColumnIndex: 0) { + d[feedID] = unreadCount + } + } + + DispatchQueue.main.async() { + completion(d) + } + } + } + func fetchStarredAndUnreadCount(_ feeds: Set, _ callback: @escaping (Int) -> Void) { if feeds.isEmpty { diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index c312cce2e..653d95003 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -57,7 +57,7 @@ public final class Database { // MARK: - Unread Counts public func fetchUnreadCounts(for feeds: Set, _ completion: @escaping UnreadCountCompletionBlock) { - + articlesTable.fetchUnreadCounts(feeds, completion) } @@ -71,6 +71,11 @@ public final class Database { articlesTable.fetchStarredAndUnreadCount(feeds, callback) } + public func fetchAllNonZeroUnreadCounts(_ completion: @escaping UnreadCountCompletionBlock) { + + articlesTable.fetchAllUnreadCounts(completion) + } + // MARK: - Saving and Updating Articles public func update(feed: Feed, parsedFeed: ParsedFeed, completion: @escaping UpdateArticlesWithFeedCompletionBlock) { diff --git a/Frameworks/Database/UnreadCountDictionary.swift b/Frameworks/Database/UnreadCountDictionary.swift index 16b731f9b..4182d16fd 100644 --- a/Frameworks/Database/UnreadCountDictionary.swift +++ b/Frameworks/Database/UnreadCountDictionary.swift @@ -12,6 +12,10 @@ import Data public struct UnreadCountDictionary { private var dictionary = [String: Int]() + + public var isEmpty: Bool { + return dictionary.count < 1 + } subscript(_ feedID: String) -> Int? { get {