Merge branch 'master' of https://github.com/brentsimmons/Evergreen
@ -6,6 +6,39 @@
|
|||||||
<description>Most recent Evergreen changes with links to updates.</description>
|
<description>Most recent Evergreen changes with links to updates.</description>
|
||||||
<language>en</language>
|
<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>
|
<item>
|
||||||
<title>Version 1.0d20</title>
|
<title>Version 1.0d20</title>
|
||||||
<description><![CDATA[
|
<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>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0d21</string>
|
<string>1.0d23</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>513</string>
|
<string>515</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
@ -10,14 +10,17 @@ import Foundation
|
|||||||
import WebKit
|
import WebKit
|
||||||
import RSCore
|
import RSCore
|
||||||
import Data
|
import Data
|
||||||
|
import RSWeb
|
||||||
|
|
||||||
class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate {
|
final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate {
|
||||||
|
|
||||||
var webview: WKWebView!
|
var webview: WKWebView!
|
||||||
|
var noSelectionView: NoSelectionView!
|
||||||
|
|
||||||
var article: Article? {
|
var article: Article? {
|
||||||
didSet {
|
didSet {
|
||||||
reloadHTML()
|
reloadHTML()
|
||||||
|
showOrHideWebView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,11 +52,16 @@ class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate
|
|||||||
webview.uiDelegate = self
|
webview.uiDelegate = self
|
||||||
webview.navigationDelegate = self
|
webview.navigationDelegate = self
|
||||||
webview.translatesAutoresizingMaskIntoConstraints = false
|
webview.translatesAutoresizingMaskIntoConstraints = false
|
||||||
let boxView = self.view as! DetailBox
|
if let userAgent = UserAgent.fromInfoPlist() {
|
||||||
boxView.contentView = webview
|
webview.customUserAgent = userAgent
|
||||||
boxView.rs_addFullSizeConstraints(forSubview: webview)
|
}
|
||||||
|
|
||||||
|
noSelectionView = NoSelectionView(frame: self.view.bounds)
|
||||||
|
|
||||||
|
let boxView = self.view as! DetailBox
|
||||||
boxView.viewController = self
|
boxView.viewController = self
|
||||||
|
|
||||||
|
showOrHideWebView()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
@ -89,6 +97,26 @@ class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// MARK: WKNavigationDelegate
|
||||||
|
|
||||||
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
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?
|
weak var viewController: DetailViewController?
|
||||||
|
|
||||||
@ -156,3 +184,25 @@ class DetailBox: NSBox {
|
|||||||
viewController?.viewDidEndLiveResize()
|
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>
|
<integer>4</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>Detail</key>
|
||||||
|
<dict>
|
||||||
|
<key>noSelectionView</key>
|
||||||
|
<dict>
|
||||||
|
<key>backgroundColor</key>
|
||||||
|
<string>FFFFFF</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -140,6 +140,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.updateUnreadCount()
|
self.updateUnreadCount()
|
||||||
|
self.fetchAllUnreadCounts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +303,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
}
|
}
|
||||||
importOPMLItems(children, parentFolder: nil)
|
importOPMLItems(children, parentFolder: nil)
|
||||||
dirty = true
|
dirty = true
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.refreshAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateUnreadCounts(for feeds: Set<Feed>) {
|
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])
|
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
|
// 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) {
|
func fetchStarredAndUnreadCount(_ feeds: Set<Feed>, _ callback: @escaping (Int) -> Void) {
|
||||||
|
|
||||||
if feeds.isEmpty {
|
if feeds.isEmpty {
|
||||||
|
@ -71,6 +71,11 @@ public final class Database {
|
|||||||
articlesTable.fetchStarredAndUnreadCount(feeds, callback)
|
articlesTable.fetchStarredAndUnreadCount(feeds, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func fetchAllNonZeroUnreadCounts(_ completion: @escaping UnreadCountCompletionBlock) {
|
||||||
|
|
||||||
|
articlesTable.fetchAllUnreadCounts(completion)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Saving and Updating Articles
|
// MARK: - Saving and Updating Articles
|
||||||
|
|
||||||
public func update(feed: Feed, parsedFeed: ParsedFeed, completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
public func update(feed: Feed, parsedFeed: ParsedFeed, completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
||||||
|
@ -13,6 +13,10 @@ public struct UnreadCountDictionary {
|
|||||||
|
|
||||||
private var dictionary = [String: Int]()
|
private var dictionary = [String: Int]()
|
||||||
|
|
||||||
|
public var isEmpty: Bool {
|
||||||
|
return dictionary.count < 1
|
||||||
|
}
|
||||||
|
|
||||||
subscript(_ feedID: String) -> Int? {
|
subscript(_ feedID: String) -> Int? {
|
||||||
get {
|
get {
|
||||||
return dictionary[feedID]
|
return dictionary[feedID]
|
||||||
|