Merge branch 'master' of https://github.com/brentsimmons/Evergreen
|
@ -5,7 +5,40 @@
|
|||
<link>https://ranchero.com/downloads/evergreen-beta.xml</link>
|
||||
<description>Most recent Evergreen changes with links to updates.</description>
|
||||
<language>en</language>
|
||||
|
||||
|
||||
<item>
|
||||
<title>Version 1.0d23</title>
|
||||
<description><![CDATA[
|
||||
<p>Decorate the tree!</p>
|
||||
]]></description>
|
||||
<pubDate>Tue, 5 Dec 2017 13:00:00 -0800</pubDate>
|
||||
<enclosure url="https://ranchero.com/downloads/Evergreen1.0d23.zip" sparkle:version="515" sparkle:shortVersionString="1.0d23" length="7306243" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.13</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Version 1.0d22</title>
|
||||
<description><![CDATA[
|
||||
<p>Refresh all after importing OPML.</p>
|
||||
<p>Fetch unread counts from database at startup.</p>
|
||||
<p>Make resizing the timeline view marginally faster.</p>
|
||||
<p>Make the favicon downloading system use a little less memory.</p>
|
||||
<p>Read a newly-added feed immediately, instead of waiting for the next refresh-all.</p>
|
||||
<p>Use 38-pt-wide toolbar icons, a la Mail.</p>
|
||||
<p>Parse RSS 1.0 (RDF) feeds. Pinboard uses these (I couldn’t find them anywhere else).</p>
|
||||
<p>Make the window title-less. You can bring back the title using a <a href="https://github.com/brentsimmons/Evergreen/blob/master/Technotes/HiddenPrefs.md">hidden pref</a>.</p>
|
||||
<p>Save feed authors.</p>
|
||||
<p>Save article authors.</p>
|
||||
<p>Use updated @2x next-unread icon from Brad.</p>
|
||||
<p>Fix bug detecting feed type for Dr. Drang’s JSON Feed.</p>
|
||||
<p>Fix bug detecting Macworld’s RSS feed as an RSS feed. The feed doesn’t start with the standard XML header.</p>
|
||||
<p>Set user-agent on the detail view’s webview.</p>
|
||||
]]></description>
|
||||
<pubDate>Mon, 4 Dec 2017 13:00:00 -0800</pubDate>
|
||||
<enclosure url="https://ranchero.com/downloads/Evergreen1.0d22.zip" sparkle:version="514" sparkle:shortVersionString="1.0d22" length="7153185" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.13</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Version 1.0d20</title>
|
||||
<description><![CDATA[
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_16x16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_32x32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_128x128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_256x256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_512x512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 308 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 371 KiB |
|
@ -17,9 +17,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0d21</string>
|
||||
<string>1.0d23</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>513</string>
|
||||
<string>515</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -108,6 +108,14 @@
|
|||
<integer>4</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>Detail</key>
|
||||
<dict>
|
||||
<key>noSelectionView</key>
|
||||
<dict>
|
||||
<key>backgroundColor</key>
|
||||
<string>FFFFFF</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
|
|
@ -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<Feed>) {
|
||||
|
@ -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
|
||||
|
|
|
@ -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<Feed>, _ callback: @escaping (Int) -> Void) {
|
||||
|
||||
if feeds.isEmpty {
|
||||
|
|
|
@ -57,7 +57,7 @@ public final class Database {
|
|||
// MARK: - Unread Counts
|
||||
|
||||
public func fetchUnreadCounts(for feeds: Set<Feed>, _ 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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|