From a13d21395e3ebe718c1d89ff9d3ffad045827ba0 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 12:07:55 -0800 Subject: [PATCH 01/84] Fetch starred articles for the Starred smart feed. --- .../SmartFeeds/StarredFeedDelegate.swift | 7 +++-- Frameworks/Account/Account.swift | 5 ++++ Frameworks/Database/ArticlesTable.swift | 27 +++++++++++++++++++ Frameworks/Database/Database.swift | 5 ++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Evergreen/SmartFeeds/StarredFeedDelegate.swift b/Evergreen/SmartFeeds/StarredFeedDelegate.swift index e09ca4db9..9d2f3bc2f 100644 --- a/Evergreen/SmartFeeds/StarredFeedDelegate.swift +++ b/Evergreen/SmartFeeds/StarredFeedDelegate.swift @@ -24,8 +24,11 @@ struct StarredFeedDelegate: SmartFeedDelegate { func fetchArticles() -> Set
{ - // TODO - return Set
() + var articles = Set
() + for account in AccountManager.shared.accounts { + articles.formUnion(account.fetchStarredArticles()) + } + return articles } func fetchUnreadArticles() -> Set
{ diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 44c334222..4853ddea2 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -364,6 +364,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return database.fetchTodayArticles(for: flattenedFeeds()) } + public func fetchStarredArticles() -> Set
{ + + return database.fetchStarredArticles(for: flattenedFeeds()) + } + private func validateUnreadCount(_ feed: Feed, _ articles: Set
) { // articles must contain all the unread articles for the feed. diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index 8bc0aeccb..ea251ab70 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -77,6 +77,11 @@ final class ArticlesTable: DatabaseTable { return fetchTodayArticles(feeds.feedIDs()) } + public func fetchStarredArticles(for feeds: Set) -> Set
{ + + return fetchStarredArticles(feeds.feedIDs()) + } + // MARK: Updating func update(_ feed: Feed, _ parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { @@ -405,6 +410,28 @@ private extension ArticlesTable { return articles } + func fetchStarredArticles(_ feedIDs: Set) -> Set
{ + + if feedIDs.isEmpty { + return Set
() + } + + var articles = Set
() + + queue.fetchSync { (database) in + + // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred = 1 and userDeleted = 0; + + let parameters = feedIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! + let whereClause = "feedID in \(placeholders) and starred = 1 and userDeleted = 0" + articles = self.fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) + } + + return articles + } + + func articlesWithSQL(_ sql: String, _ parameters: [AnyObject], _ database: FMDatabase) -> Set
{ guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else { diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index 9eb372c1c..3fc0624aa 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -62,6 +62,11 @@ public final class Database { return articlesTable.fetchTodayArticles(for: feeds) } + public func fetchStarredArticles(for feeds: Set) -> Set
{ + + return articlesTable.fetchStarredArticles(for: feeds) + } + // MARK: - Unread Counts public func fetchUnreadCounts(for feeds: Set, _ completion: @escaping UnreadCountCompletionBlock) { From c8d2fac9a6fd9cc01f5a83120c1e61c6734535d5 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 12:59:35 -0800 Subject: [PATCH 02/84] Rename MarkReadOrUnreadCommand to MarkStatusCommand and make it handle starring/unstarring and deleting/undeleting. Also: add contextual menu for smart feeds in the sidebar. --- Commands/MarkReadOrUnreadCommand.swift | 63 ------------- Commands/MarkStatusCommand.swift | 93 +++++++++++++++++++ Evergreen.xcodeproj/project.pbxproj | 8 +- ...idebarViewController+ContextualMenus.swift | 45 +++++---- ...melineViewController+ContextualMenus.swift | 2 +- .../Timeline/TimelineViewController.swift | 8 +- 6 files changed, 130 insertions(+), 89 deletions(-) delete mode 100644 Commands/MarkReadOrUnreadCommand.swift create mode 100644 Commands/MarkStatusCommand.swift diff --git a/Commands/MarkReadOrUnreadCommand.swift b/Commands/MarkReadOrUnreadCommand.swift deleted file mode 100644 index 0fb4122ec..000000000 --- a/Commands/MarkReadOrUnreadCommand.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// MarkReadOrUnreadCommand.swift -// Evergreen -// -// Created by Brent Simmons on 10/26/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import Foundation -import RSCore -import Data - -final class MarkReadOrUnreadCommand: UndoableCommand { - - static private let markReadActionName = NSLocalizedString("Mark Read", comment: "command") - static private let markUnreadActionName = NSLocalizedString("Mark Unread", comment: "command") - let undoActionName: String - let redoActionName: String - let articles: Set
- let undoManager: UndoManager - let markingRead: Bool - - init?(initialArticles: [Article], markingRead: Bool, undoManager: UndoManager) { - - // Filter out articles already read. - let articlesToMark = initialArticles.filter { markingRead ? !$0.status.read : $0.status.read } - if articlesToMark.isEmpty { - return nil - } - self.articles = Set(articlesToMark) - - self.markingRead = markingRead - - self.undoManager = undoManager - - if markingRead { - self.undoActionName = MarkReadOrUnreadCommand.markReadActionName - self.redoActionName = MarkReadOrUnreadCommand.markReadActionName - } - else { - self.undoActionName = MarkReadOrUnreadCommand.markUnreadActionName - self.redoActionName = MarkReadOrUnreadCommand.markUnreadActionName - } - } - - func perform() { - mark(read: markingRead) - registerUndo() - } - - func undo() { - mark(read: !markingRead) - registerRedo() - } -} - -private extension MarkReadOrUnreadCommand { - - func mark(read: Bool) { - - markArticles(articles, statusKey: .read, flag: read) - } -} diff --git a/Commands/MarkStatusCommand.swift b/Commands/MarkStatusCommand.swift new file mode 100644 index 000000000..b3a655e85 --- /dev/null +++ b/Commands/MarkStatusCommand.swift @@ -0,0 +1,93 @@ +// +// MarkStatusCommand.swift +// Evergreen +// +// Created by Brent Simmons on 10/26/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSCore +import Data + +// Mark articles read/unread, starred/unstarred, deleted/undeleted. + +final class MarkStatusCommand: UndoableCommand { + + let undoActionName: String + let redoActionName: String + let articles: Set
+ let undoManager: UndoManager + let flag: Bool + let statusKey: ArticleStatus.Key + + init?(initialArticles: [Article], statusKey: ArticleStatus.Key, flag: Bool, undoManager: UndoManager) { + + // Filter out articles that already have the desired status. + let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag) + if articlesToMark.isEmpty { + return nil + } + self.articles = Set(articlesToMark) + + self.flag = flag + self.statusKey = statusKey + self.undoManager = undoManager + + let actionName = MarkStatusCommand.actionName(statusKey, flag) + self.undoActionName = actionName + self.redoActionName = actionName + } + + convenience init?(initialArticles: [Article], markingRead: Bool, undoManager: UndoManager) { + + self.init(initialArticles: initialArticles, statusKey: .read, flag: markingRead, undoManager: undoManager) + } + + convenience init?(initialArticles: [Article], markingStarred: Bool, undoManager: UndoManager) { + + self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager) + } + + func perform() { + mark(statusKey, flag) + registerUndo() + } + + func undo() { + mark(statusKey, !flag) + registerRedo() + } +} + +private extension MarkStatusCommand { + + func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) { + + markArticles(articles, statusKey: statusKey, flag: flag) + } + + static private let markReadActionName = NSLocalizedString("Mark Read", comment: "command") + static private let markUnreadActionName = NSLocalizedString("Mark Unread", comment: "command") + static private let markStarredActionName = NSLocalizedString("Mark Starred", comment: "command") + static private let markUnstarredActionName = NSLocalizedString("Mark Unstarred", comment: "command") + static private let markDeletedActionName = NSLocalizedString("Delete", comment: "command") + static private let markUndeletedActionName = NSLocalizedString("Undelete", comment: "command") + + static func actionName(_ statusKey: ArticleStatus.Key, _ flag: Bool) -> String { + + switch statusKey { + case .read: + return flag ? markReadActionName : markUnreadActionName + case .starred: + return flag ? markStarredActionName : markUnstarredActionName + case .userDeleted: + return flag ? markDeletedActionName : markUndeletedActionName + } + } + + static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] { + + return articles.filter{ $0.status.boolStatus(forKey: statusKey) != flag } + } +} diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 5d46feaf2..7671a145a 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -49,7 +49,7 @@ 846E773E1F6EF67A00A165E2 /* Account.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 846E773A1F6EF5D700A165E2 /* Account.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 846E77411F6EF6A100A165E2 /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846E77211F6EF5D100A165E2 /* Database.framework */; }; 846E77421F6EF6A100A165E2 /* Database.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 846E77211F6EF5D100A165E2 /* Database.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 84702AA41FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */; }; + 84702AA41FA27AC0006B8943 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8472058020142E8900AD578B /* FeedInspectorViewController.swift */; }; 847FA121202BA34100BB56C8 /* SidebarContextualMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847FA120202BA34100BB56C8 /* SidebarContextualMenuDelegate.swift */; }; 848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; }; @@ -562,7 +562,7 @@ 845F52EC1FB2B9FC00C10BF0 /* FeedPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPasteboardWriter.swift; sourceTree = ""; }; 846E77161F6EF5D000A165E2 /* Database.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Database.xcodeproj; path = Frameworks/Database/Database.xcodeproj; sourceTree = ""; }; 846E77301F6EF5D600A165E2 /* Account.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Account.xcodeproj; path = Frameworks/Account/Account.xcodeproj; sourceTree = ""; }; - 84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkReadOrUnreadCommand.swift; sourceTree = ""; }; + 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkStatusCommand.swift; sourceTree = ""; }; 8472058020142E8900AD578B /* FeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInspectorViewController.swift; sourceTree = ""; }; 847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; 847FA120202BA34100BB56C8 /* SidebarContextualMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarContextualMenuDelegate.swift; sourceTree = ""; }; @@ -893,7 +893,7 @@ 84702AB31FA27AE8006B8943 /* Commands */ = { isa = PBXGroup; children = ( - 84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */, + 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */, 84B99C9C1FAE83C600ECDEDB /* DeleteFromSidebarCommand.swift */, 84A1500220048D660046AD9A /* SendToCommand.swift */, 84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */, @@ -1896,7 +1896,7 @@ D553738B20186C20006D8857 /* Article+Scriptability.swift in Sources */, 8403E75B201C4A79007F7246 /* FeedListKeyboardDelegate.swift in Sources */, 845EE7C11FC2488C00854A1F /* SmartFeed.swift in Sources */, - 84702AA41FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift in Sources */, + 84702AA41FA27AC0006B8943 /* MarkStatusCommand.swift in Sources */, D5907D7F2004AC00005947E5 /* NSApplication+Scriptability.swift in Sources */, 849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */, 849A97651ED9EB96007D329B /* SidebarTreeControllerDelegate.swift in Sources */, diff --git a/Evergreen/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Evergreen/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index 70e79fac6..133c6c30e 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift @@ -19,17 +19,22 @@ extension SidebarViewController { return menuForNoSelection() } - if objects.count == 1 { - if let feed = objects.first as? Feed { - return menuForFeed(feed) - } - if let folder = objects.first as? Folder { - return menuForFolder(folder) - } - return nil + if objects.count > 1 { + return menuForMultipleObjects(objects) } - return menuForMultipleObjects(objects) + let object = objects.first! + + switch object { + case is Feed: + return menuForFeed(object as! Feed) + case is Folder: + return menuForFolder(object as! Folder) + case is PseudoFeed: + return menuForSmartFeed(object as! PseudoFeed) + default: + return nil + } } } @@ -60,11 +65,7 @@ extension SidebarViewController { } let articles = unreadArticles(for: objects) - if articles.isEmpty { - return - } - - guard let undoManager = undoManager, let markReadCommand = MarkReadOrUnreadCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else { + guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else { return } runCommand(markReadCommand) @@ -160,11 +161,21 @@ private extension SidebarViewController { return menu.numberOfItems > 0 ? menu : nil } + func menuForSmartFeed(_ smartFeed: PseudoFeed) -> NSMenu? { + + let menu = NSMenu(title: "") + + if smartFeed.unreadCount > 0 { + menu.addItem(markAllReadMenuItem([smartFeed])) + } + return menu.numberOfItems > 0 ? menu : nil + } + func menuForMultipleObjects(_ objects: [Any]) -> NSMenu? { - guard allObjectsAreFeedsAndOrFolders(objects) else { - return nil - } +// guard allObjectsAreFeedsAndOrFolders(objects) else { +// return nil +// } let menu = NSMenu(title: "") diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift index 3a0df2348..44aff4df0 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -72,7 +72,7 @@ private extension TimelineViewController { guard let articlesToMark = read ? unreadArticles(from: articles) : readArticles(from: articles) else { return } - guard let undoManager = undoManager, let markReadCommand = MarkReadOrUnreadCommand(initialArticles: Array(articlesToMark), markingRead: read, undoManager: undoManager) else { + guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articlesToMark), markingRead: read, undoManager: undoManager) else { return } diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 993a8d8ba..328b8131b 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -157,7 +157,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { func markAllAsRead() { - guard let undoManager = undoManager, let markReadCommand = MarkReadOrUnreadCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else { + guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else { return } runCommand(markReadCommand) @@ -201,7 +201,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { @IBAction func markSelectedArticlesAsRead(_ sender: Any?) { - guard let undoManager = undoManager, let markReadCommand = MarkReadOrUnreadCommand(initialArticles: selectedArticles, markingRead: true, undoManager: undoManager) else { + guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: true, undoManager: undoManager) else { return } runCommand(markReadCommand) @@ -209,7 +209,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { @IBAction func markSelectedArticlesAsUnread(_ sender: Any?) { - guard let undoManager = undoManager, let markUnreadCommand = MarkReadOrUnreadCommand(initialArticles: selectedArticles, markingRead: false, undoManager: undoManager) else { + guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: false, undoManager: undoManager) else { return } runCommand(markUnreadCommand) @@ -237,7 +237,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { return } - guard let undoManager = undoManager, let markReadCommand = MarkReadOrUnreadCommand(initialArticles: articlesToMark, markingRead: true, undoManager: undoManager) else { + guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articlesToMark, markingRead: true, undoManager: undoManager) else { return } runCommand(markReadCommand) From f8e4fb4f1c359407291c9b98d98b9bf06684cca3 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 14:30:48 -0800 Subject: [PATCH 03/84] Bump version number. --- Evergreen/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evergreen/Info.plist b/Evergreen/Info.plist index 7100a1767..f43cd2e5a 100644 --- a/Evergreen/Info.plist +++ b/Evergreen/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0d35 + 1.0d36 CFBundleVersion 522 LSMinimumSystemVersion From 8967538f76aeb89b6de45772d1ad5ec307d6a15b Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 14:53:18 -0800 Subject: [PATCH 04/84] Update appcast for 1.0d36. --- Appcasts/evergreen-beta.xml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index c216a76d6..a52174e9b 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -6,6 +6,33 @@ Most recent Evergreen changes with links to updates. en + + Evergreen 1.0d36 + AppleScript! +

Evergreen is scriptable — use Script Editor to see the app’s scripting dictionary.

+ +

Sidebar

+

Fetch and display articles for smart feeds. (The Starred feed is empty for now, though, because you can’t actually star anything yet.)

+

Display a contextual menu for feeds, folders, and smart feeds.

+ +

Timeline

+

Display a contextual menu for articles.

+ +

Detail

+

Remove contextual menu items from the web view that don’t make sense for Evergren — Reload, for example.

+ +

Misc.

+

Rearrange the default toolbar to put Search in the middle.

+

Update the name of the Show/Hide Sidebar menu command when needed.

+ + ]]>
+ Sun, 11 Feb 2018 14:35:00 -0800 + + 10.13 +
+ Evergreen 1.0d35 Date: Sun, 11 Feb 2018 15:15:52 -0800 Subject: [PATCH 05/84] Remove feed preview view from Feed Directory. Punted that till after 1.0. Also: made the Feed Directory window vibrant dark. Gratuitously. --- Evergreen/FeedList/FeedList.storyboard | 147 ++---------------- .../FeedList/FeedListWindowController.swift | 1 + 2 files changed, 15 insertions(+), 133 deletions(-) diff --git a/Evergreen/FeedList/FeedList.storyboard b/Evergreen/FeedList/FeedList.storyboard index a46b0f412..f41a4293c 100644 --- a/Evergreen/FeedList/FeedList.storyboard +++ b/Evergreen/FeedList/FeedList.storyboard @@ -1,7 +1,7 @@ - + - + @@ -9,12 +9,12 @@ - + - + - + @@ -33,18 +33,18 @@ - + - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - + - + - + - + @@ -121,11 +64,11 @@ - + - + @@ -139,7 +82,7 @@ - + @@ -186,21 +129,54 @@ + + + + + + + + + + + + + + + + + - + + + + + - + diff --git a/Evergreen/FeedList/FeedListViewController.swift b/Evergreen/FeedList/FeedListViewController.swift index b885478ab..4145f94c6 100644 --- a/Evergreen/FeedList/FeedListViewController.swift +++ b/Evergreen/FeedList/FeedListViewController.swift @@ -23,12 +23,26 @@ struct FeedListUserInfoKey { final class FeedListViewController: NSViewController { @IBOutlet var outlineView: NSOutlineView! + @IBOutlet var openHomePageButton: NSButton! + @IBOutlet var addToFeedsButton: NSButton! + private var sidebarCellAppearance: SidebarCellAppearance! private let treeControllerDelegate = FeedListTreeControllerDelegate() lazy var treeController: TreeController = { TreeController(delegate: treeControllerDelegate) }() + private var selectedNodes: [Node] { + if let nodes = outlineView.selectedItems as? [Node] { + return nodes + } + return [Node]() + } + + private var selectedObjects: [AnyObject] { + return selectedNodes.representedObjects() + } + // MARK: NSViewController override func viewDidLoad() { @@ -38,6 +52,7 @@ final class FeedListViewController: NSViewController { sidebarCellAppearance = SidebarCellAppearance(theme: appDelegate.currentTheme, fontSize: AppDefaults.shared.sidebarFontSize) NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) outlineView.needsLayout = true + updateUI() } // MARK: - Notifications @@ -48,6 +63,19 @@ final class FeedListViewController: NSViewController { } } +// MARK: Actions + +extension FeedListViewController { + + func openHomePage(_ sender: Any?) { + + } + + func addToFeeds(_ sender: Any?) { + + } +} + // MARK: - NSOutlineViewDataSource extension FeedListViewController: NSOutlineViewDataSource { @@ -92,6 +120,8 @@ extension FeedListViewController: NSOutlineViewDelegate { func outlineViewSelectionDidChange(_ notification: Notification) { + updateUI() + let selectedRow = self.outlineView.selectedRow if selectedRow < 0 || selectedRow == NSNotFound { @@ -176,4 +206,37 @@ private extension FeedListViewController { NotificationCenter.default.post(name: .FeedListSidebarSelectionDidChange, object: self, userInfo: userInfo) } + + func updateUI() { + + updateButtons() + } + + func updateButtons() { + + let objects = selectedObjects + + if objects.isEmpty { + openHomePageButton.isEnabled = false + addToFeedsButton.isEnabled = false + return + } + + addToFeedsButton.isEnabled = true + + if let _ = singleSelectedHomePageURL() { + openHomePageButton.isEnabled = true + } + else { + openHomePageButton.isEnabled = false + } + } + + func singleSelectedHomePageURL() -> String? { + + guard selectedObjects.count == 1, let homePageURL = (selectedObjects.first! as? FeedListFeed)?.homePageURL, !homePageURL.isEmpty else { + return nil + } + return homePageURL + } } diff --git a/Evergreen/FeedList/FeedListWindowController.swift b/Evergreen/FeedList/FeedListWindowController.swift index 6298ba4b8..f8518dc34 100644 --- a/Evergreen/FeedList/FeedListWindowController.swift +++ b/Evergreen/FeedList/FeedListWindowController.swift @@ -10,19 +10,10 @@ import AppKit class FeedListWindowController : NSWindowController { - override func windowDidLoad() { window!.appearance = NSAppearance(named: .vibrantDark) } - - @IBAction func addToFeeds(_ sender: AnyObject) { - - } - - @IBAction func openHomePage(_ sender: AnyObject) { - - } } diff --git a/Evergreen/FeedList/FeedListControlsView.swift b/Evergreen/FeedList/UnusedIn1.0/FeedListControlsView.swift similarity index 71% rename from Evergreen/FeedList/FeedListControlsView.swift rename to Evergreen/FeedList/UnusedIn1.0/FeedListControlsView.swift index 0943bfe3b..dcbdf7d6d 100644 --- a/Evergreen/FeedList/FeedListControlsView.swift +++ b/Evergreen/FeedList/UnusedIn1.0/FeedListControlsView.swift @@ -8,8 +8,8 @@ import AppKit +// Unused, at least for now. + @objc final class FeedListControlsView: NSView { - @IBOutlet var addToFeedsButton: NSButton! - @IBOutlet var openHomePageButton: NSButton! } diff --git a/Evergreen/FeedList/FeedListKeyboardDelegate.swift b/Evergreen/FeedList/UnusedIn1.0/FeedListKeyboardDelegate.swift similarity index 100% rename from Evergreen/FeedList/FeedListKeyboardDelegate.swift rename to Evergreen/FeedList/UnusedIn1.0/FeedListKeyboardDelegate.swift diff --git a/Evergreen/FeedList/FeedListSplitViewController.swift b/Evergreen/FeedList/UnusedIn1.0/FeedListSplitViewController.swift similarity index 100% rename from Evergreen/FeedList/FeedListSplitViewController.swift rename to Evergreen/FeedList/UnusedIn1.0/FeedListSplitViewController.swift From ddf57944be66f18c0f88d02c43d229feb37aa355 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 17:13:34 -0800 Subject: [PATCH 08/84] =?UTF-8?q?Remember=20the=20Feed=20Directory=20windo?= =?UTF-8?q?w=E2=80=99s=20frame=20between=20runs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Evergreen/FeedList/FeedListWindowController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Evergreen/FeedList/FeedListWindowController.swift b/Evergreen/FeedList/FeedListWindowController.swift index f8518dc34..c1a09c961 100644 --- a/Evergreen/FeedList/FeedListWindowController.swift +++ b/Evergreen/FeedList/FeedListWindowController.swift @@ -12,7 +12,10 @@ class FeedListWindowController : NSWindowController { override func windowDidLoad() { - window!.appearance = NSAppearance(named: .vibrantDark) +// window!.appearance = NSAppearance(named: .vibrantDark) + + let windowAutosaveName = NSWindow.FrameAutosaveName(rawValue: "FeedDirectoryWindow") + window?.setFrameUsingName(windowAutosaveName, force: true) } } From f72da562e0b78c8f6bc5ed61a56166959a31ed12 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 17:14:09 -0800 Subject: [PATCH 09/84] Fix disclosure triangle padding in the Feed Directory. Implement the Open Home Page command. --- Evergreen.xcodeproj/project.pbxproj | 4 +++ Evergreen/FeedList/FeedList.storyboard | 18 +++++++---- Evergreen/FeedList/FeedListOutlineView.swift | 30 +++++++++++++++++++ .../FeedList/FeedListViewController.swift | 16 ++++++---- 4 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 Evergreen/FeedList/FeedListOutlineView.swift diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index d4a9ac507..5f05e01e3 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 842E45E31ED8C681000A8B52 /* KeyboardDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45E21ED8C681000A8B52 /* KeyboardDelegateProtocol.swift */; }; 842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */; }; 842E45E71ED8C747000A8B52 /* DB5.plist in Resources */ = {isa = PBXBuildFile; fileRef = 842E45E61ED8C747000A8B52 /* DB5.plist */; }; + 843A3B5620311E7700BF76EC /* FeedListOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843A3B5520311E7700BF76EC /* FeedListOutlineView.swift */; }; 84411E711FE5FBFA004B527F /* SmallIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */; }; 8444C8F21FED81840051386C /* OPMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8444C8F11FED81840051386C /* OPMLExporter.swift */; }; 844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B5B581FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift */; }; @@ -540,6 +541,7 @@ 842E45E21ED8C681000A8B52 /* KeyboardDelegateProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardDelegateProtocol.swift; sourceTree = ""; }; 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindowSplitView.swift; sourceTree = ""; }; 842E45E61ED8C747000A8B52 /* DB5.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = DB5.plist; path = Evergreen/Resources/DB5.plist; sourceTree = ""; }; + 843A3B5520311E7700BF76EC /* FeedListOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListOutlineView.swift; sourceTree = ""; }; 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallIconProvider.swift; sourceTree = ""; }; 8444C8F11FED81840051386C /* OPMLExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLExporter.swift; sourceTree = ""; }; 844B5B581FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarKeyboardDelegate.swift; sourceTree = ""; }; @@ -1020,6 +1022,7 @@ 84C12A141FF5B0080009A267 /* FeedList.storyboard */, 849A978C1ED9EE4D007D329B /* FeedListWindowController.swift */, 84F204CD1FAACB660076E152 /* FeedListViewController.swift */, + 843A3B5520311E7700BF76EC /* FeedListOutlineView.swift */, 84B99C661FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift */, 84B99C681FAE36B800ECDEDB /* FeedListFolder.swift */, 84B99C6A1FAE370B00ECDEDB /* FeedListFeed.swift */, @@ -1920,6 +1923,7 @@ 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, 84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */, 849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */, + 843A3B5620311E7700BF76EC /* FeedListOutlineView.swift in Sources */, 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, 84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */, 845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */, diff --git a/Evergreen/FeedList/FeedList.storyboard b/Evergreen/FeedList/FeedList.storyboard index 49f3ac362..917f8ad6a 100644 --- a/Evergreen/FeedList/FeedList.storyboard +++ b/Evergreen/FeedList/FeedList.storyboard @@ -9,7 +9,7 @@ - + @@ -33,20 +33,20 @@ - + - + - + - + @@ -82,7 +82,7 @@ - + @@ -138,6 +138,9 @@ + + + diff --git a/Evergreen/FeedList/FeedListOutlineView.swift b/Evergreen/FeedList/FeedListOutlineView.swift new file mode 100644 index 000000000..7e6966bc0 --- /dev/null +++ b/Evergreen/FeedList/FeedListOutlineView.swift @@ -0,0 +1,30 @@ +// +// FeedListOutlineView.swift +// Evergreen +// +// Created by Brent Simmons on 2/11/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit +import RSTree + +final class FeedListOutlineView: NSOutlineView { + + override func frameOfCell(atColumn column: Int, row: Int) -> NSRect { + + // Adjust top-level cells — they were too close to the disclosure indicator. + + var frame = super.frameOfCell(atColumn: column, row: row) + + let node = item(atRow: row) as! Node + guard let parentNode = node.parent, parentNode.isRoot else { + return frame + } + + let adjustment: CGFloat = 4.0 + frame.origin.x += adjustment + frame.size.width -= adjustment + return frame + } +} diff --git a/Evergreen/FeedList/FeedListViewController.swift b/Evergreen/FeedList/FeedListViewController.swift index 4145f94c6..e5c5d0928 100644 --- a/Evergreen/FeedList/FeedListViewController.swift +++ b/Evergreen/FeedList/FeedListViewController.swift @@ -67,11 +67,15 @@ final class FeedListViewController: NSViewController { extension FeedListViewController { - func openHomePage(_ sender: Any?) { + @IBAction func openHomePage(_ sender: Any?) { + guard let homePageURL = singleSelectedHomePageURL() else { + return + } + Browser.open(homePageURL, inBackground: false) } - func addToFeeds(_ sender: Any?) { + @IBAction func addToFeeds(_ sender: Any?) { } } @@ -133,8 +137,11 @@ extension FeedListViewController: NSOutlineViewDelegate { postSidebarSelectionDidChangeNotification(selectedNode.representedObject) } } +} - private func configure(_ cell: SidebarCell, _ node: Node) { +private extension FeedListViewController { + + func configure(_ cell: SidebarCell, _ node: Node) { cell.cellAppearance = sidebarCellAppearance cell.objectValue = node @@ -164,9 +171,6 @@ extension FeedListViewController: NSOutlineViewDelegate { } return "" } -} - -private extension FeedListViewController { func nodeForRow(_ row: Int) -> Node? { From 2495a882ee11b6fad5aaf535732b793a2ad8d757 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 17:42:58 -0800 Subject: [PATCH 10/84] Make the buttons at the bottom of the Feed Directory not change their width on window resize. --- Evergreen/FeedList/FeedList.storyboard | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Evergreen/FeedList/FeedList.storyboard b/Evergreen/FeedList/FeedList.storyboard index 917f8ad6a..480c7b595 100644 --- a/Evergreen/FeedList/FeedList.storyboard +++ b/Evergreen/FeedList/FeedList.storyboard @@ -34,23 +34,23 @@ - + - + - + - + - + @@ -64,11 +64,11 @@ - + - + @@ -82,7 +82,7 @@ - + @@ -130,7 +130,7 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + From 1aac355418591304f5b46ce67bf0001cc89e45a8 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 18:58:50 -0800 Subject: [PATCH 14/84] =?UTF-8?q?Make=20all=20senders=20parameters=20for?= =?UTF-8?q?=20actions=20optional=20=E2=80=94=C2=A0Any=3F=20instead=20of=20?= =?UTF-8?q?AnyObject=20or=20Any.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Evergreen-iOS/MasterViewController.swift | 2 +- Evergreen/AppDelegate.swift | 28 +++++++++---------- .../AddFeed/AddFeedWindowController.swift | 6 ++-- .../AddFolder/AddFolderWindowController.swift | 4 +-- .../MainWindow/MainWindowController.swift | 4 +-- .../Renaming/RenameWindowController.swift | 4 +-- .../Sidebar/SidebarViewController.swift | 4 +++ .../Timeline/TimelineViewController.swift | 4 +-- .../PreferencesWindowController.swift | 2 +- 9 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Evergreen-iOS/MasterViewController.swift b/Evergreen-iOS/MasterViewController.swift index 8c49a2c90..eccbba797 100644 --- a/Evergreen-iOS/MasterViewController.swift +++ b/Evergreen-iOS/MasterViewController.swift @@ -38,7 +38,7 @@ class MasterViewController: UITableViewController { } @objc - func insertNewObject(_ sender: Any) { + func insertNewObject(_ sender: Any?) { objects.insert(NSDate(), at: 0) let indexPath = IndexPath(row: 0, section: 0) tableView.insertRows(at: [indexPath], with: .automatic) diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index d103ce06c..8609606f0 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -273,7 +273,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, readerWindow.showWindow(self) } - @IBAction func showPreferences(_ sender: AnyObject) { + @IBAction func showPreferences(_ sender: Any?) { if preferencesWindowController == nil { preferencesWindowController = windowControllerWithName("Preferences") @@ -282,28 +282,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, preferencesWindowController!.showWindow(self) } - @IBAction func showMainWindow(_ sender: AnyObject) { + @IBAction func showMainWindow(_ sender: Any?) { createAndShowMainWindow() } - @IBAction func refreshAll(_ sender: AnyObject) { + @IBAction func refreshAll(_ sender: Any?) { AccountManager.shared.refreshAll() } - @IBAction func showAddFeedWindow(_ sender: AnyObject) { + @IBAction func showAddFeedWindow(_ sender: Any?) { addFeed(nil) } - @IBAction func showAddFolderWindow(_ sender: AnyObject) { + @IBAction func showAddFolderWindow(_ sender: Any?) { createAndShowMainWindow() showAddFolderSheetOnWindow(mainWindowController!.window!) } - @IBAction func showFeedList(_ sender: AnyObject) { + @IBAction func showFeedList(_ sender: Any?) { if feedListWindowController == nil { feedListWindowController = windowControllerWithName("FeedList") @@ -353,7 +353,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, logWindowController!.showWindow(self) } - @IBAction func importOPMLFromFile(_ sender: AnyObject) { + @IBAction func importOPMLFromFile(_ sender: Any?) { let panel = NSOpenPanel() panel.canDownloadUbiquitousContents = true @@ -378,11 +378,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } - @IBAction func importOPMLFromURL(_ sender: AnyObject) { + @IBAction func importOPMLFromURL(_ sender: Any?) { } - @IBAction func exportOPML(_ sender: AnyObject) { + @IBAction func exportOPML(_ sender: Any?) { let panel = NSSavePanel() panel.allowedFileTypes = ["opml"] @@ -409,7 +409,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } - @IBAction func addAppNews(_ sender: AnyObject) { + @IBAction func addAppNews(_ sender: Any?) { if AccountManager.shared.anyAccountHasFeedWithURL(appNewsURLString) { return @@ -417,17 +417,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, addFeed(appNewsURLString, "Evergreen News") } - @IBAction func openWebsite(_ sender: AnyObject) { + @IBAction func openWebsite(_ sender: Any?) { Browser.open("https://ranchero.com/evergreen/", inBackground: false) } - @IBAction func openRepository(_ sender: AnyObject) { + @IBAction func openRepository(_ sender: Any?) { Browser.open("https://github.com/brentsimmons/Evergreen", inBackground: false) } - @IBAction func openBugTracker(_ sender: AnyObject) { + @IBAction func openBugTracker(_ sender: Any?) { Browser.open("https://github.com/brentsimmons/Evergreen/issues", inBackground: false) } @@ -437,7 +437,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, Browser.open("https://github.com/brentsimmons/Evergreen/tree/master/Technotes", inBackground: false) } - @IBAction func showHelp(_ sender: AnyObject) { + @IBAction func showHelp(_ sender: Any?) { Browser.open("https://ranchero.com/evergreen/help/1.0/", inBackground: false) } diff --git a/Evergreen/MainWindow/AddFeed/AddFeedWindowController.swift b/Evergreen/MainWindow/AddFeed/AddFeedWindowController.swift index 44e8000ef..908e58ae5 100644 --- a/Evergreen/MainWindow/AddFeed/AddFeedWindowController.swift +++ b/Evergreen/MainWindow/AddFeed/AddFeedWindowController.swift @@ -87,12 +87,12 @@ class AddFeedWindowController : NSWindowController { // MARK: Actions - @IBAction func cancel(_ sender: AnyObject) { + @IBAction func cancel(_ sender: Any?) { cancelSheet() } - @IBAction func addFeed(_ sender: AnyObject) { + @IBAction func addFeed(_ sender: Any?) { let urlString = urlTextField.stringValue let normalizedURLString = (urlString as NSString).rs_normalizedURL() @@ -109,7 +109,7 @@ class AddFeedWindowController : NSWindowController { delegate?.addFeedWindowController(self, userEnteredURL: url, userEnteredTitle: userEnteredTitle, container: selectedContainer()!) } - @IBAction func localShowFeedList(_ sender: AnyObject) { + @IBAction func localShowFeedList(_ sender: Any?) { NSApplication.shared.sendAction(NSSelectorFromString("showFeedList:"), to: nil, from: sender) hostWindow.endSheet(window!, returnCode: NSApplication.ModalResponse.continue) diff --git a/Evergreen/MainWindow/AddFolder/AddFolderWindowController.swift b/Evergreen/MainWindow/AddFolder/AddFolderWindowController.swift index 15cbdeda8..8737dd3a7 100644 --- a/Evergreen/MainWindow/AddFolder/AddFolderWindowController.swift +++ b/Evergreen/MainWindow/AddFolder/AddFolderWindowController.swift @@ -68,12 +68,12 @@ class AddFolderWindowController : NSWindowController { // MARK: Actions - @IBAction func cancel(_ sender: AnyObject) { + @IBAction func cancel(_ sender: Any?) { hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) } - @IBAction func addFolder(_ sender: AnyObject) { + @IBAction func addFolder(_ sender: Any?) { hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK) } diff --git a/Evergreen/MainWindow/MainWindowController.swift b/Evergreen/MainWindow/MainWindowController.swift index 5a73960dd..026f52c7a 100644 --- a/Evergreen/MainWindow/MainWindowController.swift +++ b/Evergreen/MainWindow/MainWindowController.swift @@ -186,12 +186,12 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } - @IBAction func showAddFolderWindow(_ sender: Any) { + @IBAction func showAddFolderWindow(_ sender: Any?) { appDelegate.showAddFolderSheetOnWindow(window!) } - @IBAction func showAddFeedWindow(_ sender: Any) { + @IBAction func showAddFeedWindow(_ sender: Any?) { appDelegate.showAddFeedSheetOnWindow(window!, urlString: nil, name: nil) } diff --git a/Evergreen/MainWindow/Sidebar/Renaming/RenameWindowController.swift b/Evergreen/MainWindow/Sidebar/Renaming/RenameWindowController.swift index c88dca2c5..12ac8cd98 100644 --- a/Evergreen/MainWindow/Sidebar/Renaming/RenameWindowController.swift +++ b/Evergreen/MainWindow/Sidebar/Renaming/RenameWindowController.swift @@ -44,12 +44,12 @@ final class RenameWindowController: NSWindowController { // MARK: Actions - @IBAction func cancel(_ sender: AnyObject) { + @IBAction func cancel(_ sender: Any?) { window?.sheetParent?.endSheet(window!, returnCode: .cancel) } - @IBAction func rename(_ sender: AnyObject) { + @IBAction func rename(_ sender: Any?) { guard let representedObject = representedObject else { return diff --git a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift index 3afc39974..06b47480a 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift @@ -165,6 +165,10 @@ import RSCore outlineView.revealAndSelectRepresentedObject(SmartFeedsController.shared.starredFeed, treeController) } + @IBAction func copy(_ sender: Any?) { + + } + // MARK: Navigation func canGoToNextUnread() -> Bool { diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 3c188b886..a885c36ff 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -174,14 +174,14 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { // MARK: - Actions - @objc func openArticleInBrowser(_ sender: AnyObject) { + @objc func openArticleInBrowser(_ sender: Any?) { if let link = oneSelectedArticle?.preferredLink { Browser.open(link) } } - @IBAction func toggleStatusOfSelectedArticles(_ sender: AnyObject) { + @IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) { guard !selectedArticles.isEmpty else { return diff --git a/Evergreen/Preferences/PreferencesWindowController.swift b/Evergreen/Preferences/PreferencesWindowController.swift index ee4b83d94..dcee2fdde 100644 --- a/Evergreen/Preferences/PreferencesWindowController.swift +++ b/Evergreen/Preferences/PreferencesWindowController.swift @@ -54,7 +54,7 @@ class PreferencesWindowController : NSWindowController, NSToolbarDelegate { // MARK: Actions - @objc func toolbarItemClicked(_ sender: AnyObject) { + @objc func toolbarItemClicked(_ sender: Any?) { } From 2b6c2eb5ba44dcda82305ce46c0c4c58ff254fc0 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 21:55:32 -0800 Subject: [PATCH 15/84] Create a PasteboardWriterOwner protocol and an NSPasteboard extension that references it. --- .../RSCore/RSCore.xcodeproj/project.pbxproj | 8 ++++ .../RSCore/AppKit/NSPasteboard+RSCore.swift | 41 +++++++++++++++++++ .../RSCore/RSCore/PasteboardWriterOwner.swift | 14 +++++++ 3 files changed, 63 insertions(+) create mode 100644 Frameworks/RSCore/RSCore/AppKit/NSPasteboard+RSCore.swift create mode 100644 Frameworks/RSCore/RSCore/PasteboardWriterOwner.swift diff --git a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj index d5c387606..fb7318fe2 100755 --- a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj +++ b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj @@ -97,6 +97,8 @@ 849B08981BF7BCE30090CEE4 /* NSPasteboard+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 849B08961BF7BCE30090CEE4 /* NSPasteboard+RSCore.m */; }; 849BF8BA1C9130150071D1DA /* DiskSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849BF8B91C9130150071D1DA /* DiskSaver.swift */; }; 84A8358A1D4EC7B80004C598 /* PlistProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A835891D4EC7B80004C598 /* PlistProviderProtocol.swift */; }; + 84AD1EA520315A8800BC20B7 /* PasteboardWriterOwner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EA420315A8700BC20B7 /* PasteboardWriterOwner.swift */; }; + 84AD1EA820315BA900BC20B7 /* NSPasteboard+RSCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EA720315BA900BC20B7 /* NSPasteboard+RSCore.swift */; }; 84B890561C59CF1600D8BF23 /* NSString+ExtrasTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 84B890551C59CF1600D8BF23 /* NSString+ExtrasTests.m */; }; 84B99C941FAE64D500ECDEDB /* DisplayNameProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C931FAE64D400ECDEDB /* DisplayNameProvider.swift */; }; 84B99C951FAE64D500ECDEDB /* DisplayNameProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C931FAE64D400ECDEDB /* DisplayNameProvider.swift */; }; @@ -217,6 +219,8 @@ 849B08961BF7BCE30090CEE4 /* NSPasteboard+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSPasteboard+RSCore.m"; sourceTree = ""; }; 849BF8B91C9130150071D1DA /* DiskSaver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DiskSaver.swift; path = RSCore/DiskSaver.swift; sourceTree = ""; }; 84A835891D4EC7B80004C598 /* PlistProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlistProviderProtocol.swift; path = RSCore/PlistProviderProtocol.swift; sourceTree = ""; }; + 84AD1EA420315A8700BC20B7 /* PasteboardWriterOwner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PasteboardWriterOwner.swift; path = RSCore/PasteboardWriterOwner.swift; sourceTree = ""; }; + 84AD1EA720315BA900BC20B7 /* NSPasteboard+RSCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSPasteboard+RSCore.swift"; path = "AppKit/NSPasteboard+RSCore.swift"; sourceTree = ""; }; 84B890551C59CF1600D8BF23 /* NSString+ExtrasTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+ExtrasTests.m"; sourceTree = ""; }; 84B99C931FAE64D400ECDEDB /* DisplayNameProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DisplayNameProvider.swift; path = RSCore/DisplayNameProvider.swift; sourceTree = ""; }; 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OPMLRepresentable.swift; path = RSCore/OPMLRepresentable.swift; sourceTree = ""; }; @@ -364,6 +368,7 @@ 8402047D1FBCE77900D94C1A /* BatchUpdate.swift */, 848F6AE81FC2BC50002D422E /* ThreadSafeCache.swift */, 844B5B561FE9D36000C7C76A /* Keyboard.swift */, + 84AD1EA420315A8700BC20B7 /* PasteboardWriterOwner.swift */, 84CFF5241AC3C8A200CEA6C8 /* Foundation */, 84CFF5551AC3CF4A00CEA6C8 /* AppKit */, 84CFF5661AC3D13F00CEA6C8 /* Images */, @@ -480,6 +485,7 @@ 84C687371FBC028900345C9E /* LogItem.swift */, 8434D15B200BD6F400D6281E /* UserApp.swift */, 84D5BA1D201E87E2009092BD /* URLPasteboardWriter.swift */, + 84AD1EA720315BA900BC20B7 /* NSPasteboard+RSCore.swift */, 842DD7F91E1499FA00E061EB /* Views */, ); name = AppKit; @@ -789,6 +795,7 @@ 84C687381FBC028900345C9E /* LogItem.swift in Sources */, 8432B1861DACA0E90057D6DF /* NSResponder-Extensions.swift in Sources */, 84E8E0D9202EC39800562D8F /* NSMenu+Extensions.swift in Sources */, + 84AD1EA820315BA900BC20B7 /* NSPasteboard+RSCore.swift in Sources */, 84D5BA1E201E87E2009092BD /* URLPasteboardWriter.swift in Sources */, 849B08981BF7BCE30090CEE4 /* NSPasteboard+RSCore.m in Sources */, 842635571D7FA1C800196285 /* NSTableView+Extensions.swift in Sources */, @@ -810,6 +817,7 @@ 842635591D7FA24800196285 /* NSOutlineView+Extensions.swift in Sources */, 844C915C1B65753E0051FC1B /* RSPlist.m in Sources */, 84CFF5231AC3C89D00CEA6C8 /* NSObject+RSCore.m in Sources */, + 84AD1EA520315A8800BC20B7 /* PasteboardWriterOwner.swift in Sources */, 8414CBA71C95F2EA00333C12 /* Set+Extensions.swift in Sources */, 84B99C9A1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */, 84E34DA61F9FA1070077082F /* UndoableCommand.swift in Sources */, diff --git a/Frameworks/RSCore/RSCore/AppKit/NSPasteboard+RSCore.swift b/Frameworks/RSCore/RSCore/AppKit/NSPasteboard+RSCore.swift new file mode 100644 index 000000000..d3efe491c --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/NSPasteboard+RSCore.swift @@ -0,0 +1,41 @@ +// +// NSPasteboard+RSCore.swift +// RSCore +// +// Created by Brent Simmons on 2/11/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import AppKit + +public extension NSPasteboard { + + func copyObjects(_ objects: [Any]) { + + guard let writers = writersFor(objects) else { + return + } + + clearContents() + writeObjects(writers) + } + + func canCopyAtLeastOneObject(_ objects: [Any]) -> Bool { + + for object in objects { + if object is PasteboardWriterOwner { + return true + } + } + return false + } +} + +private extension NSPasteboard { + + func writersFor(_ objects: [Any]) -> [NSPasteboardWriting]? { + + let writers = objects.compactMap { ($0 as? PasteboardWriterOwner)?.pasteboardWriter } + return writers.isEmpty ? nil : writers + } +} diff --git a/Frameworks/RSCore/RSCore/PasteboardWriterOwner.swift b/Frameworks/RSCore/RSCore/PasteboardWriterOwner.swift new file mode 100644 index 000000000..354e2f434 --- /dev/null +++ b/Frameworks/RSCore/RSCore/PasteboardWriterOwner.swift @@ -0,0 +1,14 @@ +// +// PasteboardWriterOwner.swift +// RSCore +// +// Created by Brent Simmons on 2/11/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import AppKit + +public protocol PasteboardWriterOwner { + + var pasteboardWriter: NSPasteboardWriting { get } +} From 81e56ba84b9a24159f343022e28350147068ff62 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 11 Feb 2018 22:10:28 -0800 Subject: [PATCH 16/84] Implement and validate the Copy command for the sidebar. Fix #115. --- Evergreen.xcodeproj/project.pbxproj | 8 ++ .../Sidebar/FeedPasteboardWriter.swift | 8 ++ .../Sidebar/FolderPasteboardWriter.swift | 82 +++++++++++++++++++ .../Sidebar/SidebarViewController.swift | 16 +++- .../Sidebar/SmartFeedPasteboardWriter.swift | 43 ++++++++++ Evergreen/SmartFeeds/PseudoFeed.swift | 2 +- Evergreen/SmartFeeds/SmartFeed.swift | 4 + Evergreen/SmartFeeds/UnreadFeed.swift | 6 +- 8 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 Evergreen/MainWindow/Sidebar/FolderPasteboardWriter.swift create mode 100644 Evergreen/MainWindow/Sidebar/SmartFeedPasteboardWriter.swift diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 5f05e01e3..1c56ee375 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -103,6 +103,8 @@ 84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A37CB4201ECD610087C5AF /* RenameWindowController.swift */; }; 84A37CBB201ECE590087C5AF /* RenameSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84A37CB9201ECE590087C5AF /* RenameSheet.xib */; }; 84AAF2BF202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */; }; + 84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */; }; + 84AD1EBA2031649C00BC20B7 /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; }; 84B06FAE1ED37DBD00F0B54B /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; }; 84B06FAF1ED37DBD00F0B54B /* RSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 84B06FB21ED37DBD00F0B54B /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06F9D1ED37DA000F0B54B /* RSDatabase.framework */; }; @@ -622,6 +624,8 @@ 84A6B6931FB8D43C006754AC /* DinosaursWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DinosaursWindow.xib; sourceTree = ""; }; 84A6B6951FB8DBD2006754AC /* DinosaursWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DinosaursWindowController.swift; sourceTree = ""; }; 84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContextualMenuDelegate.swift; sourceTree = ""; }; + 84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderPasteboardWriter.swift; sourceTree = ""; }; + 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedPasteboardWriter.swift; sourceTree = ""; }; 84B06F961ED37DA000F0B54B /* RSDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSDatabase.xcodeproj; path = Frameworks/RSDatabase/RSDatabase.xcodeproj; sourceTree = ""; }; 84B06FA21ED37DAC00F0B54B /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = Frameworks/RSCore/RSCore.xcodeproj; sourceTree = ""; }; 84B06FB61ED37E8B00F0B54B /* RSWeb.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSWeb.xcodeproj; path = Frameworks/RSWeb/RSWeb.xcodeproj; sourceTree = ""; }; @@ -953,6 +957,8 @@ 849A97611ED9EB96007D329B /* SidebarTreeControllerDelegate.swift */, 849A97631ED9EB96007D329B /* UnreadCountView.swift */, 845F52EC1FB2B9FC00C10BF0 /* FeedPasteboardWriter.swift */, + 84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */, + 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */, 849A97821ED9EC63007D329B /* SidebarStatusBarView.swift */, 84D5BA1F201E8FB6009092BD /* SidebarGearMenuDelegate.swift */, 847FA120202BA34100BB56C8 /* SidebarContextualMenuDelegate.swift */, @@ -1895,6 +1901,7 @@ 842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */, 84F2D53A1FC2308B00998D64 /* UnreadFeed.swift in Sources */, 845A29221FC9251E007B49E3 /* SidebarCellLayout.swift in Sources */, + 84AD1EBA2031649C00BC20B7 /* SmartFeedPasteboardWriter.swift in Sources */, 84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */, 849A97661ED9EB96007D329B /* SidebarViewController.swift in Sources */, 849A97641ED9EB96007D329B /* SidebarOutlineView.swift in Sources */, @@ -1925,6 +1932,7 @@ 849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */, 843A3B5620311E7700BF76EC /* FeedListOutlineView.swift in Sources */, 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, + 84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */, 84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */, 845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */, 845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */, diff --git a/Evergreen/MainWindow/Sidebar/FeedPasteboardWriter.swift b/Evergreen/MainWindow/Sidebar/FeedPasteboardWriter.swift index 6554d0281..80369f621 100644 --- a/Evergreen/MainWindow/Sidebar/FeedPasteboardWriter.swift +++ b/Evergreen/MainWindow/Sidebar/FeedPasteboardWriter.swift @@ -8,6 +8,14 @@ import AppKit import Data +import RSCore + +extension Feed: PasteboardWriterOwner { + + public var pasteboardWriter: NSPasteboardWriting { + return FeedPasteboardWriter(feed: self) + } +} @objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting { diff --git a/Evergreen/MainWindow/Sidebar/FolderPasteboardWriter.swift b/Evergreen/MainWindow/Sidebar/FolderPasteboardWriter.swift new file mode 100644 index 000000000..0b12bb676 --- /dev/null +++ b/Evergreen/MainWindow/Sidebar/FolderPasteboardWriter.swift @@ -0,0 +1,82 @@ +// +// FolderPasteboardWriter.swift +// Evergreen +// +// Created by Brent Simmons on 2/11/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit +import Account +import RSCore + +extension Folder: PasteboardWriterOwner { + + public var pasteboardWriter: NSPasteboardWriting { + return FolderPasteboardWriter(folder: self) + } +} + +@objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting { + + private let folder: Folder + static let folderUTIInternal = "com.ranchero.evergreen.internal.folder" + static let folderUTIInternalType = NSPasteboard.PasteboardType(rawValue: folderUTIInternal) + + init(folder: Folder) { + + self.folder = folder + } + + // MARK: - NSPasteboardWriting + + func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] { + + return [.string, FolderPasteboardWriter.folderUTIInternalType] + } + + func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { + + let plist: Any? + + switch type { + case .string: + plist = folder.nameForDisplay + case FolderPasteboardWriter.folderUTIInternalType: + plist = internalDictionary() + default: + plist = nil + } + + return plist + } +} + +private extension FolderPasteboardWriter { + + private struct Key { + + static let name = "name" + + // Internal + static let accountID = "accountID" + static let folderID = "folderID" + } + + func internalDictionary() -> [String: Any] { + + var d = [String: Any]() + + d[Key.folderID] = folder.folderID + if let name = folder.name { + d[Key.name] = name + } + if let accountID = folder.account?.accountID { + d[Key.accountID] = accountID + } + + return d + + } +} + diff --git a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift index 06b47480a..5b99b589d 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift @@ -167,8 +167,9 @@ import RSCore @IBAction func copy(_ sender: Any?) { + NSPasteboard.general.copyObjects(selectedObjects) } - + // MARK: Navigation func canGoToNextUnread() -> Bool { @@ -307,6 +308,19 @@ import RSCore } } +// MARK: NSUserInterfaceValidations + +extension SidebarViewController: NSUserInterfaceValidations { + + func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { + + if item.action == #selector(copy(_:)) { + return NSPasteboard.general.canCopyAtLeastOneObject(selectedObjects) + } + return true + } +} + //MARK: - Private private extension SidebarViewController { diff --git a/Evergreen/MainWindow/Sidebar/SmartFeedPasteboardWriter.swift b/Evergreen/MainWindow/Sidebar/SmartFeedPasteboardWriter.swift new file mode 100644 index 000000000..0d716c9f5 --- /dev/null +++ b/Evergreen/MainWindow/Sidebar/SmartFeedPasteboardWriter.swift @@ -0,0 +1,43 @@ +// +// SmartFeedPasteboardWriter.swift +// Evergreen +// +// Created by Brent Simmons on 2/11/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit +import Account +import RSCore + +@objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting { + + private let smartFeed: PseudoFeed + + init(smartFeed: PseudoFeed) { + + self.smartFeed = smartFeed + } + + // MARK: - NSPasteboardWriting + + func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] { + + return [.string] + } + + func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { + + let plist: Any? + + switch type { + case .string: + plist = smartFeed.nameForDisplay + default: + plist = nil + } + + return plist + } +} + diff --git a/Evergreen/SmartFeeds/PseudoFeed.swift b/Evergreen/SmartFeeds/PseudoFeed.swift index aca8b0c4a..c84cf8d6e 100644 --- a/Evergreen/SmartFeeds/PseudoFeed.swift +++ b/Evergreen/SmartFeeds/PseudoFeed.swift @@ -10,7 +10,7 @@ import Foundation import Data import RSCore -protocol PseudoFeed: class, DisplayNameProvider, UnreadCountProvider, SmallIconProvider { +protocol PseudoFeed: class, DisplayNameProvider, UnreadCountProvider, SmallIconProvider, PasteboardWriterOwner { } diff --git a/Evergreen/SmartFeeds/SmartFeed.swift b/Evergreen/SmartFeeds/SmartFeed.swift index 99de45209..5086f5c2b 100644 --- a/Evergreen/SmartFeeds/SmartFeed.swift +++ b/Evergreen/SmartFeeds/SmartFeed.swift @@ -32,6 +32,10 @@ final class SmartFeed: PseudoFeed { } } + var pasteboardWriter: NSPasteboardWriting { + return SmartFeedPasteboardWriter(smartFeed: self) + } + private let delegate: SmartFeedDelegate private var timer: Timer? private var unreadCounts = [Account: Int]() diff --git a/Evergreen/SmartFeeds/UnreadFeed.swift b/Evergreen/SmartFeeds/UnreadFeed.swift index eae26a601..98c69bb68 100644 --- a/Evergreen/SmartFeeds/UnreadFeed.swift +++ b/Evergreen/SmartFeeds/UnreadFeed.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Ranchero Software. All rights reserved. // -import Foundation +import AppKit import Account import Data @@ -24,6 +24,10 @@ final class UnreadFeed: PseudoFeed { } } + var pasteboardWriter: NSPasteboardWriting { + return SmartFeedPasteboardWriter(smartFeed: self) + } + init() { self.unreadCount = appDelegate.unreadCount From 09b8cd7811d34bf9bfda8acf044f6a56cb8f2a27 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 12 Feb 2018 13:04:07 -0800 Subject: [PATCH 17/84] Support the Copy command in the timeline. Fix #114. --- .../Timeline/ArticlePasteboardWriter.swift | 8 ++++++++ .../Timeline/TimelineViewController.swift | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Evergreen/MainWindow/Timeline/ArticlePasteboardWriter.swift b/Evergreen/MainWindow/Timeline/ArticlePasteboardWriter.swift index a3f904b05..a34d5147c 100644 --- a/Evergreen/MainWindow/Timeline/ArticlePasteboardWriter.swift +++ b/Evergreen/MainWindow/Timeline/ArticlePasteboardWriter.swift @@ -8,6 +8,14 @@ import AppKit import Data +import RSCore + +extension Article: PasteboardWriterOwner { + + public var pasteboardWriter: NSPasteboardWriting { + return ArticlePasteboardWriter(article: self) + } +} @objc final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting { diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index a885c36ff..8267e5738 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -214,6 +214,11 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { runCommand(markUnreadCommand) } + @IBAction func copy(_ sender: Any?) { + + NSPasteboard.general.copyObjects(selectedArticles) + } + func markOlderArticlesAsRead() { // Mark articles the same age or older than the selected article(s) as read. @@ -422,6 +427,19 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { } } +// MARK: NSUserInterfaceValidations + +extension TimelineViewController: NSUserInterfaceValidations { + + func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { + + if item.action == #selector(copy(_:)) { + return NSPasteboard.general.canCopyAtLeastOneObject(selectedArticles) + } + return true + } +} + // MARK: - NSTableViewDataSource extension TimelineViewController: NSTableViewDataSource { From 9adf047525bbebf7de5973f58778b8a9c211e088 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 12 Feb 2018 13:10:13 -0800 Subject: [PATCH 18/84] Add Donate to App Camp for Girls menu item to the Help menu. It opens the browser to https://appcamp4girls.com/contribute/ Fix #181. --- Evergreen/AppDelegate.swift | 5 +++++ Evergreen/Base.lproj/Main.storyboard | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index 8609606f0..2756f2dec 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -442,6 +442,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, Browser.open("https://ranchero.com/evergreen/help/1.0/", inBackground: false) } + @IBAction func donateToAppCampForGirls(_ sender: Any?) { + + Browser.open("https://appcamp4girls.com/contribute/", inBackground: false) + } + @IBAction func debugDropConditionalGetInfo(_ sender: Any?) { #if DEBUG AccountManager.shared.accounts.forEach{ $0.debugDropConditionalGetInfo() } diff --git a/Evergreen/Base.lproj/Main.storyboard b/Evergreen/Base.lproj/Main.storyboard index 01e847f32..b34254e0c 100644 --- a/Evergreen/Base.lproj/Main.storyboard +++ b/Evergreen/Base.lproj/Main.storyboard @@ -1,7 +1,7 @@ - + - + @@ -538,6 +538,13 @@ + + + + + + + From e773df33e3d1de132f7c3bcdee352e280284966f Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 12 Feb 2018 13:31:43 -0800 Subject: [PATCH 19/84] Start work on saving main window state. --- Evergreen/AppDelegate.swift | 5 ++--- Evergreen/MainWindow/MainWindowController.swift | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index 2756f2dec..1e3062ac3 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -499,9 +499,8 @@ private extension AppDelegate { func saveState() { - if let inspectorWindowController = inspectorWindowController { - inspectorWindowController.saveState() - } + inspectorWindowController?.saveState() + mainWindowController?.saveState() } func updateSortMenuItems() { diff --git a/Evergreen/MainWindow/MainWindowController.swift b/Evergreen/MainWindow/MainWindowController.swift index 026f52c7a..46063dfaa 100644 --- a/Evergreen/MainWindow/MainWindowController.swift +++ b/Evergreen/MainWindow/MainWindowController.swift @@ -75,6 +75,13 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } } + func saveState() { + + // TODO: save width of split view and anything else that should be saved. + + + } + // MARK: Sidebar func selectedObjectsInSidebar() -> [AnyObject]? { From f2228120b5c93599e986241288c56717fed08cde Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 12 Feb 2018 22:02:51 -0800 Subject: [PATCH 20/84] Make SidebarOutlineDataSource a separate object. Move data source methods out of SidebarViewController. --- Evergreen.xcodeproj/project.pbxproj | 4 ++ Evergreen/Base.lproj/MainWindow.storyboard | 1 - .../Sidebar/SidebarOutlineDataSource.swift | 57 +++++++++++++++++++ .../Sidebar/SidebarViewController.swift | 37 +++--------- 4 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 Evergreen/MainWindow/Sidebar/SidebarOutlineDataSource.swift diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 1c56ee375..644e771f9 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -105,6 +105,7 @@ 84AAF2BF202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */; }; 84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */; }; 84AD1EBA2031649C00BC20B7 /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; }; + 84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */; }; 84B06FAE1ED37DBD00F0B54B /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; }; 84B06FAF1ED37DBD00F0B54B /* RSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 84B06FB21ED37DBD00F0B54B /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06F9D1ED37DA000F0B54B /* RSDatabase.framework */; }; @@ -626,6 +627,7 @@ 84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContextualMenuDelegate.swift; sourceTree = ""; }; 84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderPasteboardWriter.swift; sourceTree = ""; }; 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedPasteboardWriter.swift; sourceTree = ""; }; + 84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarOutlineDataSource.swift; sourceTree = ""; }; 84B06F961ED37DA000F0B54B /* RSDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSDatabase.xcodeproj; path = Frameworks/RSDatabase/RSDatabase.xcodeproj; sourceTree = ""; }; 84B06FA21ED37DAC00F0B54B /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = Frameworks/RSCore/RSCore.xcodeproj; sourceTree = ""; }; 84B06FB61ED37E8B00F0B54B /* RSWeb.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSWeb.xcodeproj; path = Frameworks/RSWeb/RSWeb.xcodeproj; sourceTree = ""; }; @@ -953,6 +955,7 @@ children = ( 849A97621ED9EB96007D329B /* SidebarViewController.swift */, 84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */, + 84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */, 849A97601ED9EB96007D329B /* SidebarOutlineView.swift */, 849A97611ED9EB96007D329B /* SidebarTreeControllerDelegate.swift */, 849A97631ED9EB96007D329B /* UnreadCountView.swift */, @@ -1934,6 +1937,7 @@ 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, 84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */, 84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */, + 84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */, 845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */, 845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */, 848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */, diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index aa07aa7ad..588bc5c37 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -337,7 +337,6 @@ - diff --git a/Evergreen/MainWindow/Sidebar/SidebarOutlineDataSource.swift b/Evergreen/MainWindow/Sidebar/SidebarOutlineDataSource.swift new file mode 100644 index 000000000..5b8b30e88 --- /dev/null +++ b/Evergreen/MainWindow/Sidebar/SidebarOutlineDataSource.swift @@ -0,0 +1,57 @@ +// +// SidebarOutlineDataSource.swift +// Evergreen +// +// Created by Brent Simmons on 2/12/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit +import RSTree +import Data +import RSCore + +@objc final class SidebarOutlineDataSource: NSObject, NSOutlineViewDataSource { + + let treeController: TreeController + + init(treeController: TreeController) { + self.treeController = treeController + } + + // MARK: - NSOutlineViewDataSource + + func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { + + return nodeForItem(item as AnyObject?).numberOfChildNodes + } + + func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { + + return nodeForItem(item as AnyObject?).childNodes![index] + } + + func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { + + return nodeForItem(item as AnyObject?).canHaveChildNodes + } + + func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { + + let node = nodeForItem(item as AnyObject?) + return (node.representedObject as? PasteboardWriterOwner)?.pasteboardWriter + } +} + +// MARK: - Private + +private extension SidebarOutlineDataSource { + + func nodeForItem(_ item: AnyObject?) -> Node { + + if item == nil { + return treeController.rootNode + } + return item as! Node + } +} diff --git a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift index 5b99b589d..f3a97ca8b 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift @@ -20,8 +20,12 @@ import RSCore let treeControllerDelegate = SidebarTreeControllerDelegate() lazy var treeController: TreeController = { - TreeController(delegate: treeControllerDelegate) + return TreeController(delegate: treeControllerDelegate) }() + lazy var dataSource: SidebarOutlineDataSource = { + return SidebarOutlineDataSource(treeController: treeController) + }() + var undoableCommands = [UndoableCommand]() private var animatingChanges = false private var sidebarCellAppearance: SidebarCellAppearance! @@ -38,6 +42,7 @@ import RSCore sidebarCellAppearance = SidebarCellAppearance(theme: appDelegate.currentTheme, fontSize: AppDefaults.shared.sidebarFontSize) + outlineView.dataSource = dataSource outlineView.setDraggingSourceOperationMask(.move, forLocal: true) outlineView.setDraggingSourceOperationMask(.copy, forLocal: false) @@ -270,7 +275,7 @@ import RSCore // TODO: support multiple selection let selectedRow = self.outlineView.selectedRow - + if selectedRow < 0 || selectedRow == NSNotFound { postSidebarSelectionDidChangeNotification(nil) return @@ -280,35 +285,9 @@ import RSCore postSidebarSelectionDidChangeNotification([selectedNode.representedObject]) } } - - // MARK: NSOutlineViewDataSource - - func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { - - return nodeForItem(item as AnyObject?).numberOfChildNodes - } - - func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { - - return nodeForItem(item as AnyObject?).childNodes![index] - } - - func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { - - return nodeForItem(item as AnyObject?).canHaveChildNodes - } - - func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { - - let node = nodeForItem(item as AnyObject?) - if let feed = node.representedObject as? Feed { - return FeedPasteboardWriter(feed: feed) - } - return nil - } } -// MARK: NSUserInterfaceValidations +// MARK: - NSUserInterfaceValidations extension SidebarViewController: NSUserInterfaceValidations { From 0762074e919339362fbf6f4e535b20f2121c9819 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 12 Feb 2018 22:13:37 -0800 Subject: [PATCH 21/84] Support display of articles in the timeline from multiple items selected in the sidebar. As a side effect: fix #295. --- .../MainWindow/Sidebar/SidebarViewController.swift | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift index f3a97ca8b..07b4004c4 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift @@ -272,18 +272,7 @@ import RSCore func outlineViewSelectionDidChange(_ notification: Notification) { - // TODO: support multiple selection - - let selectedRow = self.outlineView.selectedRow - - if selectedRow < 0 || selectedRow == NSNotFound { - postSidebarSelectionDidChangeNotification(nil) - return - } - - if let selectedNode = self.outlineView.item(atRow: selectedRow) as? Node { - postSidebarSelectionDidChangeNotification([selectedNode.representedObject]) - } + postSidebarSelectionDidChangeNotification(selectedObjects.isEmpty ? nil : selectedObjects) } } From 192439abe7c4ffd0c41232df448335356d2fa60f Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 12 Feb 2018 22:22:06 -0800 Subject: [PATCH 22/84] =?UTF-8?q?Make=20Folder=20watch=20for=20children-di?= =?UTF-8?q?d-change=20notifications=20=E2=80=94=20when=20its=20own=20child?= =?UTF-8?q?ren=20change,=20update=20the=20unread=20count.=20Fix=20#322.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frameworks/Account/Folder.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index 85d411b06..33928ea0a 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -58,6 +58,7 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, self.hashValue = folderID NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: self) } // MARK: - Disk Dictionary @@ -130,7 +131,7 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, return true } - // MARK: Notifications + // MARK: - Notifications @objc func unreadCountDidChange(_ note: Notification) { @@ -141,6 +142,11 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, } } + @objc func childrenDidChange(_ note: Notification) { + + updateUnreadCount() + } + // MARK: - Equatable static public func ==(lhs: Folder, rhs: Folder) -> Bool { From 2f21dbf6beb23bda3862c412fcb0dc4e89054afa Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 14 Feb 2018 13:14:25 -0800 Subject: [PATCH 23/84] Remove extraneous get { from a bunch of read-only accessors. --- Commands/DeleteFromSidebarCommand.swift | 18 ++-- Evergreen/Data/ArticleUtilities.swift | 16 +--- Evergreen/FeedList/FeedListFeed.swift | 4 +- Evergreen/FeedList/FeedListFolder.swift | 4 +- .../Inspector/InspectorWindowController.swift | 4 +- .../AddFeed/AddFeedController.swift | 10 +-- .../AddFeed/AddFeedWindowController.swift | 12 ++- .../MainWindow/MainWindowSplitView.swift | 8 +- .../MainWindow/Sidebar/Cell/SidebarCell.swift | 4 +- .../Sidebar/SidebarStatusBarView.swift | 6 +- .../MainWindow/Sidebar/UnreadCountView.swift | 30 +++---- .../Timeline/TimelineTableRowView.swift | 15 ++-- .../Timeline/TimelineViewController.swift | 12 +-- .../PreferencesWindowController.swift | 4 +- Evergreen/SmartFeeds/PseudoFeed.swift | 4 +- Evergreen/SmartFeeds/SmartFeed.swift | 4 +- Frameworks/Account/Account.swift | 14 ++- Frameworks/Account/AccountManager.swift | 30 +++---- Frameworks/Account/DataExtensions.swift | 12 +-- .../Feedbin/FeedbinAccountDelegate.swift | 4 +- Frameworks/Account/Folder.swift | 63 ++++++------- .../LocalAccount/LocalAccountDelegate.swift | 4 +- .../LocalAccount/LocalAccountRefresher.swift | 4 +- Frameworks/Data/Feed.swift | 90 +++++++++---------- .../Extensions/Article+Database.swift | 4 +- .../Extensions/ArticleStatus+Database.swift | 4 +- .../Extensions/Attachment+Database.swift | 4 +- .../Database/Extensions/Author+Database.swift | 4 +- .../Extensions/ParsedArticle+Database.swift | 10 +-- Frameworks/Database/StatusesTable.swift | 4 +- .../Database/UnreadCountDictionary.swift | 4 +- Frameworks/RSCore/RSCore/BatchUpdate.swift | 4 +- .../RSCore/NSOutlineView+Extensions.swift | 13 ++- .../RSCore/NSTableView+Extensions.swift | 8 +- .../Related Objects/RelatedObjectIDsMap.swift | 4 +- .../Related Objects/RelatedObjectsMap.swift | 4 +- .../RSFeedFinder/FeedSpecifier.swift | 4 +- .../RSFeedFinder/HTMLFeedFinder.swift | 12 ++- Frameworks/RSTree/RSTree/Node.swift | 42 ++++----- Frameworks/RSWeb/RSWeb/DownloadObject.swift | 4 +- Frameworks/RSWeb/RSWeb/DownloadProgress.swift | 20 ++--- Frameworks/RSWeb/RSWeb/DownloadSession.swift | 4 +- .../RSWeb/RSWeb/HTTPConditionalGetInfo.swift | 18 ++-- .../RSWeb/RSWeb/URLResponse+RSWeb.swift | 22 ++--- 44 files changed, 216 insertions(+), 353 deletions(-) diff --git a/Commands/DeleteFromSidebarCommand.swift b/Commands/DeleteFromSidebarCommand.swift index 7047c8578..5652bf672 100644 --- a/Commands/DeleteFromSidebarCommand.swift +++ b/Commands/DeleteFromSidebarCommand.swift @@ -17,9 +17,7 @@ final class DeleteFromSidebarCommand: UndoableCommand { let undoManager: UndoManager let undoActionName: String var redoActionName: String { - get { - return undoActionName - } + return undoActionName } private let itemSpecifiers: [SidebarItemSpecifier] @@ -94,15 +92,13 @@ private struct SidebarItemSpecifier { private let path: ContainerPath private var container: Container? { - get { - if let parentFolder = parentFolder { - return parentFolder - } - if let account = account { - return account - } - return nil + if let parentFolder = parentFolder { + return parentFolder } + if let account = account { + return account + } + return nil } init?(node: Node) { diff --git a/Evergreen/Data/ArticleUtilities.swift b/Evergreen/Data/ArticleUtilities.swift index 67d0f1780..c34b22731 100644 --- a/Evergreen/Data/ArticleUtilities.swift +++ b/Evergreen/Data/ArticleUtilities.swift @@ -42,26 +42,18 @@ private func accountAndArticlesDictionary(_ articles: Set
) -> [String: extension Article { var feed: Feed? { - get { - return account?.existingFeed(with: feedID) - } + return account?.existingFeed(with: feedID) } var preferredLink: String? { - get { - return url ?? externalURL - } + return url ?? externalURL } var body: String? { - get { - return contentHTML ?? contentText ?? summary - } + return contentHTML ?? contentText ?? summary } var logicalDatePublished: Date { - get { - return datePublished ?? dateModified ?? status.dateArrived - } + return datePublished ?? dateModified ?? status.dateArrived } } diff --git a/Evergreen/FeedList/FeedListFeed.swift b/Evergreen/FeedList/FeedListFeed.swift index 80968687b..cb6839b7a 100644 --- a/Evergreen/FeedList/FeedListFeed.swift +++ b/Evergreen/FeedList/FeedListFeed.swift @@ -31,9 +31,7 @@ final class FeedListFeed: Hashable, DisplayNameProvider { } var nameForDisplay: String { // DisplayNameProvider - get { - return name - } + return name } init(name: String, url: String, homePageURL: String) { diff --git a/Evergreen/FeedList/FeedListFolder.swift b/Evergreen/FeedList/FeedListFolder.swift index c73cdc16d..ec02eff26 100644 --- a/Evergreen/FeedList/FeedListFolder.swift +++ b/Evergreen/FeedList/FeedListFolder.swift @@ -17,9 +17,7 @@ final class FeedListFolder: Hashable, DisplayNameProvider { let hashValue: Int var nameForDisplay: String { // DisplayNameProvider - get { - return name - } + return name } init(name: String, feeds: Set) { diff --git a/Evergreen/Inspector/InspectorWindowController.swift b/Evergreen/Inspector/InspectorWindowController.swift index 28588447d..0b965024e 100644 --- a/Evergreen/Inspector/InspectorWindowController.swift +++ b/Evergreen/Inspector/InspectorWindowController.swift @@ -33,9 +33,7 @@ final class InspectorWindowController: NSWindowController { } var isOpen: Bool { - get { - return isWindowLoaded && window!.isVisible - } + return isWindowLoaded && window!.isVisible } private var inspectors: [InspectorViewController]! diff --git a/Evergreen/MainWindow/AddFeed/AddFeedController.swift b/Evergreen/MainWindow/AddFeed/AddFeedController.swift index d177f9030..7c79a7331 100644 --- a/Evergreen/MainWindow/AddFeed/AddFeedController.swift +++ b/Evergreen/MainWindow/AddFeed/AddFeedController.swift @@ -126,14 +126,12 @@ class AddFeedController: AddFeedWindowControllerDelegate, FeedFinderDelegate { private extension AddFeedController { var urlStringFromPasteboard: String? { - get { - if let urlString = NSPasteboard.rs_urlString(from: NSPasteboard.general) { - return urlString.rs_normalizedURL() - } - return nil + if let urlString = NSPasteboard.rs_urlString(from: NSPasteboard.general) { + return urlString.rs_normalizedURL() } + return nil } - + struct AccountAndFolderSpecifier { let account: Account let folder: Folder? diff --git a/Evergreen/MainWindow/AddFeed/AddFeedWindowController.swift b/Evergreen/MainWindow/AddFeed/AddFeedWindowController.swift index 908e58ae5..e741f0c18 100644 --- a/Evergreen/MainWindow/AddFeed/AddFeedWindowController.swift +++ b/Evergreen/MainWindow/AddFeed/AddFeedWindowController.swift @@ -33,14 +33,12 @@ class AddFeedWindowController : NSWindowController { fileprivate var folderTreeController: TreeController! private var userEnteredTitle: String? { - get { - var s = nameTextField.stringValue - s = s.rs_stringWithCollapsedWhitespace() - if s.isEmpty { - return nil - } - return s + var s = nameTextField.stringValue + s = s.rs_stringWithCollapsedWhitespace() + if s.isEmpty { + return nil } + return s } var hostWindow: NSWindow! diff --git a/Evergreen/MainWindow/MainWindowSplitView.swift b/Evergreen/MainWindow/MainWindowSplitView.swift index e7e21ca97..e6fc261a8 100644 --- a/Evergreen/MainWindow/MainWindowSplitView.swift +++ b/Evergreen/MainWindow/MainWindowSplitView.swift @@ -9,12 +9,10 @@ import AppKit class MainWindowSplitView: NSSplitView { - + private let splitViewDividerColor = NSColor(calibratedWhite: 0.65, alpha: 1.0) - + override var dividerColor: NSColor { - get { - return splitViewDividerColor - } + return splitViewDividerColor } } diff --git a/Evergreen/MainWindow/Sidebar/Cell/SidebarCell.swift b/Evergreen/MainWindow/Sidebar/Cell/SidebarCell.swift index ae7f93ba7..07b15d5f5 100644 --- a/Evergreen/MainWindow/Sidebar/Cell/SidebarCell.swift +++ b/Evergreen/MainWindow/Sidebar/Cell/SidebarCell.swift @@ -75,9 +75,7 @@ class SidebarCell : NSTableCellView { } override var isFlipped: Bool { - get { - return true - } + return true } private func commonInit() { diff --git a/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift b/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift index 0da539ef1..3bbf5e8a4 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift @@ -25,11 +25,9 @@ final class SidebarStatusBarView: NSView { } override var isFlipped: Bool { - get { - return true - } + return true } - + override func awakeFromNib() { progressIndicator.isHidden = true diff --git a/Evergreen/MainWindow/Sidebar/UnreadCountView.swift b/Evergreen/MainWindow/Sidebar/UnreadCountView.swift index eec7f7f73..e479bf3d3 100644 --- a/Evergreen/MainWindow/Sidebar/UnreadCountView.swift +++ b/Evergreen/MainWindow/Sidebar/UnreadCountView.swift @@ -27,36 +27,30 @@ class UnreadCountView : NSView { } } var unreadCountString: String { - get { - return unreadCount < 1 ? "" : "\(unreadCount)" - } + return unreadCount < 1 ? "" : "\(unreadCount)" } private var intrinsicContentSizeIsValid = false private var _intrinsicContentSize = NSZeroSize override var intrinsicContentSize: NSSize { - get { - if !intrinsicContentSizeIsValid { - var size = NSZeroSize - if unreadCount > 0 { - size = textSize() - size.width += (padding.left + padding.right) - size.height += (padding.top + padding.bottom) - } - _intrinsicContentSize = size - intrinsicContentSizeIsValid = true + if !intrinsicContentSizeIsValid { + var size = NSZeroSize + if unreadCount > 0 { + size = textSize() + size.width += (padding.left + padding.right) + size.height += (padding.top + padding.bottom) } - return _intrinsicContentSize + _intrinsicContentSize = size + intrinsicContentSizeIsValid = true } + return _intrinsicContentSize } override var isFlipped: Bool { - get { - return true - } + return true } - + override func invalidateIntrinsicContentSize() { intrinsicContentSizeIsValid = false diff --git a/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift b/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift index 772396041..312fc92ee 100644 --- a/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift +++ b/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift @@ -23,14 +23,12 @@ class TimelineTableRowView : NSTableRowView { // } private var cellView: TimelineTableCellView? { - get { - for oneSubview in subviews { - if let foundView = oneSubview as? TimelineTableCellView { - return foundView - } + for oneSubview in subviews { + if let foundView = oneSubview as? TimelineTableCellView { + return foundView } - return nil } + return nil } override var isEmphasized: Bool { @@ -50,10 +48,7 @@ class TimelineTableRowView : NSTableRowView { } var gridRect: NSRect { - get { -// return NSMakeRect(floor(cellAppearance.boxLeftMargin), NSMaxY(bounds) - 1.0, NSWidth(bounds), 1) - return NSMakeRect(0.0, NSMaxY(bounds) - 1.0, NSWidth(bounds), 1) - } + return NSMakeRect(0.0, NSMaxY(bounds) - 1.0, NSWidth(bounds), 1) } override func drawSeparator(in dirtyRect: NSRect) { diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 8267e5738..98fcc179c 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -18,15 +18,11 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { @IBOutlet var contextualMenuDelegate: TimelineContextualMenuDelegate? var selectedArticles: [Article] { - get { - return Array(articles.articlesForIndexes(tableView.selectedRowIndexes)) - } + return Array(articles.articlesForIndexes(tableView.selectedRowIndexes)) } var hasAtLeastOneSelectedArticle: Bool { - get { - return tableView.selectedRow != -1 - } + return tableView.selectedRow != -1 } var articles = ArticleArray() { @@ -105,9 +101,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { } private var oneSelectedArticle: Article? { - get { - return selectedArticles.count == 1 ? selectedArticles.first : nil - } + return selectedArticles.count == 1 ? selectedArticles.first : nil } override func viewDidLoad() { diff --git a/Evergreen/Preferences/PreferencesWindowController.swift b/Evergreen/Preferences/PreferencesWindowController.swift index dcee2fdde..423808f41 100644 --- a/Evergreen/Preferences/PreferencesWindowController.swift +++ b/Evergreen/Preferences/PreferencesWindowController.swift @@ -96,9 +96,7 @@ class PreferencesWindowController : NSWindowController, NSToolbarDelegate { private extension PreferencesWindowController { var currentView: NSView? { - get { - return window?.contentView?.subviews.first - } + return window?.contentView?.subviews.first } func toolbarItemSpec(for identifier: String) -> PreferencesToolbarItemSpec? { diff --git a/Evergreen/SmartFeeds/PseudoFeed.swift b/Evergreen/SmartFeeds/PseudoFeed.swift index c84cf8d6e..19098fef6 100644 --- a/Evergreen/SmartFeeds/PseudoFeed.swift +++ b/Evergreen/SmartFeeds/PseudoFeed.swift @@ -22,8 +22,6 @@ private var smartFeedIcon: NSImage = { extension PseudoFeed { var smallIcon: NSImage? { - get { - return smartFeedIcon - } + return smartFeedIcon } } diff --git a/Evergreen/SmartFeeds/SmartFeed.swift b/Evergreen/SmartFeeds/SmartFeed.swift index 5086f5c2b..176875e9b 100644 --- a/Evergreen/SmartFeeds/SmartFeed.swift +++ b/Evergreen/SmartFeeds/SmartFeed.swift @@ -19,9 +19,7 @@ protocol SmartFeedDelegate: DisplayNameProvider, ArticleFetcher { final class SmartFeed: PseudoFeed { var nameForDisplay: String { - get { - return delegate.nameForDisplay - } + return delegate.nameForDisplay } var unreadCount = 0 { diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 4853ddea2..7c4aa6ca3 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -102,19 +102,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } var refreshProgress: DownloadProgress { - get { - return delegate.refreshProgress - } + return delegate.refreshProgress } - + var supportsSubFolders: Bool { - get { - return delegate.supportsSubFolders - } + return delegate.supportsSubFolders } - + init?(dataFolder: String, settingsFile: String, type: AccountType, accountID: String) { - + // TODO: support various syncing systems. precondition(type == .onMyMac) self.delegate = LocalAccountDelegate() diff --git a/Frameworks/Account/AccountManager.swift b/Frameworks/Account/AccountManager.swift index 5be8ff9a7..724a2fa50 100644 --- a/Frameworks/Account/AccountManager.swift +++ b/Frameworks/Account/AccountManager.swift @@ -31,37 +31,29 @@ public final class AccountManager: UnreadCountProvider { } public var accounts: [Account] { - get { - return Array(accountsDictionary.values) - } + return Array(accountsDictionary.values) } public var sortedAccounts: [Account] { - get { - return accountsSortedByName() - } + return accountsSortedByName() } public var refreshInProgress: Bool { - get { - for account in accounts { - if account.refreshInProgress { - return true - } + for account in accounts { + if account.refreshInProgress { + return true } - return false } + return false } - + public var combinedRefreshProgress: CombinedRefreshProgress { - get { - let downloadProgressArray = accounts.map { $0.refreshProgress } - return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray) - } + let downloadProgressArray = accounts.map { $0.refreshProgress } + return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray) } - + public init() { - + // The local "On My Mac" account must always exist, even if it's empty. let localAccountFolder = (accountsFolder as NSString).appendingPathComponent("OnMyMac") diff --git a/Frameworks/Account/DataExtensions.swift b/Frameworks/Account/DataExtensions.swift index 7f813cba7..2721c724e 100644 --- a/Frameworks/Account/DataExtensions.swift +++ b/Frameworks/Account/DataExtensions.swift @@ -18,9 +18,7 @@ public extension Notification.Name { public extension Feed { public var account: Account? { - get { - return AccountManager.shared.existingAccount(with: accountID) - } + return AccountManager.shared.existingAccount(with: accountID) } public func takeSettings(from parsedFeed: ParsedFeed) { @@ -59,15 +57,11 @@ public extension Feed { public extension Article { public var account: Account? { - get { - return AccountManager.shared.existingAccount(with: accountID) - } + return AccountManager.shared.existingAccount(with: accountID) } public var feed: Feed? { - get { - return account?.existingFeed(with: feedID) - } + return account?.existingFeed(with: feedID) } } diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index 8083562e6..1fea57d98 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -14,9 +14,7 @@ final class FeedbinAccountDelegate: AccountDelegate { let supportsSubFolders = false var refreshProgress: DownloadProgress { - get { - return DownloadProgress(numberOfTasks: 0) // TODO - } + return DownloadProgress(numberOfTasks: 0) // TODO } func refreshAll(for: Account) { diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index 33928ea0a..8bf9b756c 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -29,10 +29,7 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, // MARK: - DisplayNameProvider public var nameForDisplay: String { - get { - return name ?? Folder.untitledName - - } + return name ?? Folder.untitledName } // MARK: - UnreadCountProvider @@ -84,38 +81,36 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, } var dictionary: [String: Any] { - get { - - var d = [String: Any]() - guard let account = account else { - return d - } - - if let name = name { - d[Key.name] = name - } - if unreadCount > 0 { - d[Key.unreadCount] = unreadCount - } - - let childObjects = children.compactMap { (child) -> [String: Any]? in - - if let feed = child as? Feed { - return feed.dictionary - } - if let folder = child as? Folder, account.supportsSubFolders { - return folder.dictionary - } - assertionFailure("Expected a feed or a folder."); - return nil - } - - if !childObjects.isEmpty { - d[Key.children] = childObjects - } - + + var d = [String: Any]() + guard let account = account else { return d } + + if let name = name { + d[Key.name] = name + } + if unreadCount > 0 { + d[Key.unreadCount] = unreadCount + } + + let childObjects = children.compactMap { (child) -> [String: Any]? in + + if let feed = child as? Feed { + return feed.dictionary + } + if let folder = child as? Folder, account.supportsSubFolders { + return folder.dictionary + } + assertionFailure("Expected a feed or a folder."); + return nil + } + + if !childObjects.isEmpty { + d[Key.children] = childObjects + } + + return d } // MARK: Feeds diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index cdd636101..f981e0c22 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -15,9 +15,7 @@ final class LocalAccountDelegate: AccountDelegate { private let refresher = LocalAccountRefresher() var refreshProgress: DownloadProgress { - get { - return refresher.progress - } + return refresher.progress } func refreshAll(for account: Account) { diff --git a/Frameworks/Account/LocalAccount/LocalAccountRefresher.swift b/Frameworks/Account/LocalAccount/LocalAccountRefresher.swift index dce4d3a78..933ffff3b 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountRefresher.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountRefresher.swift @@ -19,9 +19,7 @@ final class LocalAccountRefresher { }() var progress: DownloadProgress { - get { - return downloadSession.progress - } + return downloadSession.progress } public func refreshFeeds(_ feeds: Set) { diff --git a/Frameworks/Data/Feed.swift b/Frameworks/Data/Feed.swift index c634537e3..41e83cce8 100644 --- a/Frameworks/Data/Feed.swift +++ b/Frameworks/Data/Feed.swift @@ -34,15 +34,13 @@ public final class Feed: DisplayNameProvider, UnreadCountProvider, Hashable { // MARK: - DisplayNameProvider public var nameForDisplay: String { - get { - if let s = editedName, !s.isEmpty { - return s - } - if let s = name, !s.isEmpty { - return s - } - return NSLocalizedString("Untitled", comment: "Feed name") + if let s = editedName, !s.isEmpty { + return s } + if let s = name, !s.isEmpty { + return s + } + return NSLocalizedString("Untitled", comment: "Feed name") } // MARK: - UnreadCountProvider @@ -115,46 +113,44 @@ public final class Feed: DisplayNameProvider, UnreadCountProvider, Hashable { } public var dictionary: [String: Any] { - get { - var d = [String: Any]() - - d[Key.url] = url - - // feedID is not repeated when it’s the same as url - if (feedID != url) { - d[Key.feedID] = feedID - } - - if let homePageURL = homePageURL { - d[Key.homePageURL] = homePageURL - } - if let iconURL = iconURL { - d[Key.iconURL] = iconURL - } - if let faviconURL = faviconURL { - d[Key.faviconURL] = faviconURL - } - if let name = name { - d[Key.name] = name - } - if let editedName = editedName { - d[Key.editedName] = editedName - } - if let authorsArray = authors?.diskArray() { - d[Key.authors] = authorsArray - } - if let contentHash = contentHash { - d[Key.contentHash] = contentHash - } - if unreadCount > 0 { - d[Key.unreadCount] = unreadCount - } - if let conditionalGetInfo = conditionalGetInfo { - d[Key.conditionalGetInfo] = conditionalGetInfo.dictionary - } - - return d + var d = [String: Any]() + + d[Key.url] = url + + // feedID is not repeated when it’s the same as url + if (feedID != url) { + d[Key.feedID] = feedID } + + if let homePageURL = homePageURL { + d[Key.homePageURL] = homePageURL + } + if let iconURL = iconURL { + d[Key.iconURL] = iconURL + } + if let faviconURL = faviconURL { + d[Key.faviconURL] = faviconURL + } + if let name = name { + d[Key.name] = name + } + if let editedName = editedName { + d[Key.editedName] = editedName + } + if let authorsArray = authors?.diskArray() { + d[Key.authors] = authorsArray + } + if let contentHash = contentHash { + d[Key.contentHash] = contentHash + } + if unreadCount > 0 { + d[Key.unreadCount] = unreadCount + } + if let conditionalGetInfo = conditionalGetInfo { + d[Key.conditionalGetInfo] = conditionalGetInfo.dictionary + } + + return d } // MARK: - Debug diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index b92f0f1b0..8d3d0e8b4 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -110,9 +110,7 @@ extension Article: DatabaseObject { } public var databaseID: String { - get { - return articleID - } + return articleID } public func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? { diff --git a/Frameworks/Database/Extensions/ArticleStatus+Database.swift b/Frameworks/Database/Extensions/ArticleStatus+Database.swift index b7f791f87..728b41b5f 100644 --- a/Frameworks/Database/Extensions/ArticleStatus+Database.swift +++ b/Frameworks/Database/Extensions/ArticleStatus+Database.swift @@ -26,9 +26,7 @@ extension ArticleStatus { extension ArticleStatus: DatabaseObject { public var databaseID: String { - get { - return articleID - } + return articleID } public func databaseDictionary() -> NSDictionary? { diff --git a/Frameworks/Database/Extensions/Attachment+Database.swift b/Frameworks/Database/Extensions/Attachment+Database.swift index 134cc14a3..3540dc824 100644 --- a/Frameworks/Database/Extensions/Attachment+Database.swift +++ b/Frameworks/Database/Extensions/Attachment+Database.swift @@ -57,9 +57,7 @@ private func optionalIntForColumn(_ row: FMResultSet, _ columnName: String) -> I extension Attachment: DatabaseObject { public var databaseID: String { - get { - return attachmentID - } + return attachmentID } public func databaseDictionary() -> NSDictionary? { diff --git a/Frameworks/Database/Extensions/Author+Database.swift b/Frameworks/Database/Extensions/Author+Database.swift index f28b4be4c..a885e81cc 100644 --- a/Frameworks/Database/Extensions/Author+Database.swift +++ b/Frameworks/Database/Extensions/Author+Database.swift @@ -45,9 +45,7 @@ extension Author { extension Author: DatabaseObject { public var databaseID: String { - get { - return authorID - } + return authorID } public func databaseDictionary() -> NSDictionary? { diff --git a/Frameworks/Database/Extensions/ParsedArticle+Database.swift b/Frameworks/Database/Extensions/ParsedArticle+Database.swift index 6069f0899..d232e4b79 100644 --- a/Frameworks/Database/Extensions/ParsedArticle+Database.swift +++ b/Frameworks/Database/Extensions/ParsedArticle+Database.swift @@ -13,12 +13,10 @@ import Data extension ParsedItem { var articleID: String { - get { - if let s = syncServiceID { - return s - } - // Must be same calculation as for Article. - return Article.calculatedArticleID(feedID: feedURL, uniqueID: uniqueID) + if let s = syncServiceID { + return s } + // Must be same calculation as for Article. + return Article.calculatedArticleID(feedID: feedURL, uniqueID: uniqueID) } } diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index 421911294..a5ab9eb75 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -179,9 +179,7 @@ private final class StatusCache { var dictionary = [String: ArticleStatus]() var cachedStatuses: Set { - get { - return Set(dictionary.values) - } + return Set(dictionary.values) } func add(_ statuses: Set) { diff --git a/Frameworks/Database/UnreadCountDictionary.swift b/Frameworks/Database/UnreadCountDictionary.swift index 4182d16fd..a467ec009 100644 --- a/Frameworks/Database/UnreadCountDictionary.swift +++ b/Frameworks/Database/UnreadCountDictionary.swift @@ -27,8 +27,6 @@ public struct UnreadCountDictionary { } public subscript(_ feed: Feed) -> Int? { - get { - return dictionary[feed.feedID] - } + return dictionary[feed.feedID] } } diff --git a/Frameworks/RSCore/RSCore/BatchUpdate.swift b/Frameworks/RSCore/RSCore/BatchUpdate.swift index 37f41d1b3..45e21fe65 100644 --- a/Frameworks/RSCore/RSCore/BatchUpdate.swift +++ b/Frameworks/RSCore/RSCore/BatchUpdate.swift @@ -22,9 +22,7 @@ public final class BatchUpdate { private var count = 0 public var isPerforming: Bool { - get { - return count > 0 - } + return count > 0 } public func perform(_ batchUpdateBlock: BatchUpdateBlock) { diff --git a/Frameworks/RSCore/RSCore/NSOutlineView+Extensions.swift b/Frameworks/RSCore/RSCore/NSOutlineView+Extensions.swift index 4590e702f..00e67b3f4 100755 --- a/Frameworks/RSCore/RSCore/NSOutlineView+Extensions.swift +++ b/Frameworks/RSCore/RSCore/NSOutlineView+Extensions.swift @@ -11,15 +11,12 @@ import AppKit public extension NSOutlineView { var selectedItems: [AnyObject] { - get { + if selectionIsEmpty { + return [AnyObject]() + } - if selectionIsEmpty { - return [AnyObject]() - } - - return selectedRowIndexes.compactMap { (oneIndex) -> AnyObject? in - return item(atRow: oneIndex) as AnyObject - } + return selectedRowIndexes.compactMap { (oneIndex) -> AnyObject? in + return item(atRow: oneIndex) as AnyObject } } diff --git a/Frameworks/RSCore/RSCore/NSTableView+Extensions.swift b/Frameworks/RSCore/RSCore/NSTableView+Extensions.swift index 61eb4c3b6..eb2d4ad04 100755 --- a/Frameworks/RSCore/RSCore/NSTableView+Extensions.swift +++ b/Frameworks/RSCore/RSCore/NSTableView+Extensions.swift @@ -9,13 +9,11 @@ import AppKit public extension NSTableView { - + var selectionIsEmpty: Bool { - get { - return selectedRowIndexes.startIndex == selectedRowIndexes.endIndex - } + return selectedRowIndexes.startIndex == selectedRowIndexes.endIndex } - + func indexesOfAvailableRowsPassingTest(_ test: (Int) -> Bool) -> IndexSet? { // Checks visible and in-flight rows. diff --git a/Frameworks/RSDatabase/Related Objects/RelatedObjectIDsMap.swift b/Frameworks/RSDatabase/Related Objects/RelatedObjectIDsMap.swift index 6548ff374..bc9f1247f 100644 --- a/Frameworks/RSDatabase/Related Objects/RelatedObjectIDsMap.swift +++ b/Frameworks/RSDatabase/Related Objects/RelatedObjectIDsMap.swift @@ -52,9 +52,7 @@ struct RelatedObjectIDsMap { } subscript(_ objectID: String) -> Set? { - get { - return dictionary[objectID] - } + return dictionary[objectID] } } diff --git a/Frameworks/RSDatabase/Related Objects/RelatedObjectsMap.swift b/Frameworks/RSDatabase/Related Objects/RelatedObjectsMap.swift index bc75e4926..5afd9b54c 100644 --- a/Frameworks/RSDatabase/Related Objects/RelatedObjectsMap.swift +++ b/Frameworks/RSDatabase/Related Objects/RelatedObjectsMap.swift @@ -39,8 +39,6 @@ public struct RelatedObjectsMap { } public subscript(_ objectID: String) -> [DatabaseObject]? { - get { - return dictionary[objectID] - } + return dictionary[objectID] } } diff --git a/Frameworks/RSFeedFinder/RSFeedFinder/FeedSpecifier.swift b/Frameworks/RSFeedFinder/RSFeedFinder/FeedSpecifier.swift index b2beb7b68..8ea5f59bf 100644 --- a/Frameworks/RSFeedFinder/RSFeedFinder/FeedSpecifier.swift +++ b/Frameworks/RSFeedFinder/RSFeedFinder/FeedSpecifier.swift @@ -25,9 +25,7 @@ public struct FeedSpecifier: Hashable { public let source: Source public let hashValue: Int public var score: Int { - get { - return calculatedScore() - } + return calculatedScore() } init(title: String?, urlString: String, source: Source) { diff --git a/Frameworks/RSFeedFinder/RSFeedFinder/HTMLFeedFinder.swift b/Frameworks/RSFeedFinder/RSFeedFinder/HTMLFeedFinder.swift index 7a675d9f2..fd3ad6ba5 100644 --- a/Frameworks/RSFeedFinder/RSFeedFinder/HTMLFeedFinder.swift +++ b/Frameworks/RSFeedFinder/RSFeedFinder/HTMLFeedFinder.swift @@ -12,17 +12,15 @@ import RSParser private let feedURLWordsToMatch = ["feed", "xml", "rss", "atom", "json"] class HTMLFeedFinder { - + var feedSpecifiers: Set { - get { - return Set(feedSpecifiersDictionary.values) - } + return Set(feedSpecifiersDictionary.values) } - + fileprivate var feedSpecifiersDictionary = [String: FeedSpecifier]() - + init(parserData: ParserData) { - + let metadata = RSHTMLMetadataParser.htmlMetadata(with: parserData) for oneFeedLink in metadata.feedLinks { diff --git a/Frameworks/RSTree/RSTree/Node.swift b/Frameworks/RSTree/RSTree/Node.swift index d562842e9..8a05ab274 100644 --- a/Frameworks/RSTree/RSTree/Node.swift +++ b/Frameworks/RSTree/RSTree/Node.swift @@ -21,50 +21,40 @@ public final class Node: Hashable { private static var incrementingID = 0 public var isRoot: Bool { - get { - if let _ = parent { - return false - } - return true + if let _ = parent { + return false } + return true } public var numberOfChildNodes: Int { - get { - return childNodes?.count ?? 0 - } + return childNodes?.count ?? 0 } public var indexPath: IndexPath { - get { - if let parent = parent { - let parentPath = parent.indexPath - if let childIndex = parent.indexOfChild(self) { - return parentPath.appending(childIndex) - } - preconditionFailure("A Node’s parent must contain it as a child.") + if let parent = parent { + let parentPath = parent.indexPath + if let childIndex = parent.indexOfChild(self) { + return parentPath.appending(childIndex) } - return IndexPath(index: 0) //root node + preconditionFailure("A Node’s parent must contain it as a child.") } + return IndexPath(index: 0) //root node } public var level: Int { - get { - if let parent = parent { - return parent.level + 1 - } - return 0 + if let parent = parent { + return parent.level + 1 } + return 0 } public var isLeaf: Bool { - get { - return numberOfChildNodes < 1 - } + return numberOfChildNodes < 1 } - + public init(representedObject: AnyObject, parent: Node?) { - + precondition(Thread.isMainThread) self.representedObject = representedObject diff --git a/Frameworks/RSWeb/RSWeb/DownloadObject.swift b/Frameworks/RSWeb/RSWeb/DownloadObject.swift index 47d04c8a2..394362d32 100755 --- a/Frameworks/RSWeb/RSWeb/DownloadObject.swift +++ b/Frameworks/RSWeb/RSWeb/DownloadObject.swift @@ -14,9 +14,7 @@ public final class DownloadObject: Hashable { public var data = Data() public var hashValue: Int { - get { - return url.hashValue - } + return url.hashValue } public init(url: URL) { diff --git a/Frameworks/RSWeb/RSWeb/DownloadProgress.swift b/Frameworks/RSWeb/RSWeb/DownloadProgress.swift index c3f0d7dc1..fcaefc0d4 100755 --- a/Frameworks/RSWeb/RSWeb/DownloadProgress.swift +++ b/Frameworks/RSWeb/RSWeb/DownloadProgress.swift @@ -35,22 +35,18 @@ public final class DownloadProgress { } public var numberCompleted: Int { - get { - var n = numberOfTasks - numberRemaining - if n < 0 { - n = 0 - } - if n > numberOfTasks { - n = numberOfTasks - } - return n + var n = numberOfTasks - numberRemaining + if n < 0 { + n = 0 } + if n > numberOfTasks { + n = numberOfTasks + } + return n } public var isComplete: Bool { - get { - return numberRemaining < 1 - } + return numberRemaining < 1 } public init(numberOfTasks: Int) { diff --git a/Frameworks/RSWeb/RSWeb/DownloadSession.swift b/Frameworks/RSWeb/RSWeb/DownloadSession.swift index 0a9c052c4..004fb3ca3 100755 --- a/Frameworks/RSWeb/RSWeb/DownloadSession.swift +++ b/Frameworks/RSWeb/RSWeb/DownloadSession.swift @@ -294,9 +294,7 @@ private final class DownloadInfo { var canceled = false var statusCode: Int { - get { - return urlResponse?.forcedStatusCode ?? 0 - } + return urlResponse?.forcedStatusCode ?? 0 } init(_ representedObject: AnyObject, urlRequest: URLRequest) { diff --git a/Frameworks/RSWeb/RSWeb/HTTPConditionalGetInfo.swift b/Frameworks/RSWeb/RSWeb/HTTPConditionalGetInfo.swift index 0c04fd796..639782266 100755 --- a/Frameworks/RSWeb/RSWeb/HTTPConditionalGetInfo.swift +++ b/Frameworks/RSWeb/RSWeb/HTTPConditionalGetInfo.swift @@ -12,18 +12,16 @@ public struct HTTPConditionalGetInfo { public let lastModified: String? public let etag: String? - + public var dictionary: [String: String] { - get { - var d = [String: String]() - if let lastModified = lastModified { - d[HTTPResponseHeader.lastModified] = lastModified - } - if let etag = etag { - d[HTTPResponseHeader.etag] = etag - } - return d + var d = [String: String]() + if let lastModified = lastModified { + d[HTTPResponseHeader.lastModified] = lastModified } + if let etag = etag { + d[HTTPResponseHeader.etag] = etag + } + return d } public init?(lastModified: String?, etag: String?) { diff --git a/Frameworks/RSWeb/RSWeb/URLResponse+RSWeb.swift b/Frameworks/RSWeb/RSWeb/URLResponse+RSWeb.swift index f537c442e..69d4fd415 100755 --- a/Frameworks/RSWeb/RSWeb/URLResponse+RSWeb.swift +++ b/Frameworks/RSWeb/RSWeb/URLResponse+RSWeb.swift @@ -9,23 +9,19 @@ import Foundation public extension URLResponse { - + public var statusIsOK: Bool { - get { - return forcedStatusCode >= 200 && forcedStatusCode <= 299 - } + return forcedStatusCode >= 200 && forcedStatusCode <= 299 } - + public var forcedStatusCode: Int { - - get { - // Return actual statusCode or -1 if there isn’t one. - - if let response = self as? HTTPURLResponse { - return response.statusCode - } - return 0 + + // Return actual statusCode or -1 if there isn’t one. + + if let response = self as? HTTPURLResponse { + return response.statusCode } + return 0 } } From 71e38bfb3bc5bd7ca7e0928702ab8b66597115dd Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 14 Feb 2018 13:18:47 -0800 Subject: [PATCH 24/84] Update the copyright date in Info.plist, which fixes it in the About box. --- Evergreen/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evergreen/Info.plist b/Evergreen/Info.plist index f43cd2e5a..da6da6219 100644 --- a/Evergreen/Info.plist +++ b/Evergreen/Info.plist @@ -23,7 +23,7 @@ LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright - Copyright © 2017 Ranchero Software, LLC. All rights reserved. + Copyright © 2017-2018 Ranchero Software, LLC. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass From 22d335d4adb68daf81aa55ffbaba04a0070fdbb2 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 14 Feb 2018 13:21:40 -0800 Subject: [PATCH 25/84] Bump version number. --- Evergreen/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evergreen/Info.plist b/Evergreen/Info.plist index da6da6219..fb2fb13d6 100644 --- a/Evergreen/Info.plist +++ b/Evergreen/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0d36 + 1.0d37 CFBundleVersion 522 LSMinimumSystemVersion From 4d6b15049a264871bddc40a303007ffd7c60409d Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 14 Feb 2018 13:32:10 -0800 Subject: [PATCH 26/84] Update appcast. --- Appcasts/evergreen-beta.xml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index a52174e9b..139fc1b21 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -6,6 +6,30 @@ Most recent Evergreen changes with links to updates. en + + Evergreen 1.0d37 + Misc. +

Make the Copy command work in the sidebar and timeline.

+

Add Donate to App Camp for Girls item to the Help menu. The Help menu is now a place where you can help.

+

When selecting multiple items in the sidebar, show articles in the timeline from the entire selection, not just from one thing.

+

Update folder unread count when a feed it contains is deleted.

+

Update copyright date in About box.

+

Disallow blurring behind the title bar, since it’s buggy.

+

Don’t clear undo stack when sidebar changes selection.

+

Remember Feed Directory’s window frame between runs.

+ +

Puntings

+

Font size is punted till after 1.0, and its preferences have been removed until then.

+

Feed Directory is simplified — the feed preview feature has been punted till after 1.0.

+ + ]]>
+ Wed, 14 Feb 2018 13:25:00 -0800 + + 10.13 +
+ Evergreen 1.0d36 Date: Wed, 14 Feb 2018 20:56:02 -0800 Subject: [PATCH 27/84] When detecting and parsing a potential JSON Feed, allow for the version URL to have the wrong scheme, as it does (at this writing) in https://pxlnv.com/feed/json/ Fix #347. --- .../RSParser/Feeds/JSON/JSONFeedParser.swift | 4 +- .../RSParser.xcodeproj/project.pbxproj | 6 + .../RSParserTests/FeedParserTypeTests.swift | 7 + .../RSParserTests/JSONFeedParserTests.swift | 8 + .../RSParserTests/Resources/pxlnv.json | 249 ++++++++++++++++++ .../RSParser/Utilities/NSData+RSParser.m | 2 +- 6 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 Frameworks/RSParser/RSParserTests/Resources/pxlnv.json diff --git a/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift b/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift index b1546b0e8..c77c9dc58 100644 --- a/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift +++ b/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift @@ -45,7 +45,7 @@ public struct JSONFeedParser { static let durationInSeconds = "duration_in_seconds" } - static let jsonFeedVersionPrefix = "https://jsonfeed.org/version/" + static let jsonFeedVersionMarker = "://jsonfeed.org/version/" // Allow for the mistake of not getting the scheme exactly correct. public static func parse(_ parserData: ParserData) throws -> ParsedFeed? { @@ -53,7 +53,7 @@ public struct JSONFeedParser { throw FeedParserError(.invalidJSON) } - guard let version = d[Key.version] as? String, version.hasPrefix(JSONFeedParser.jsonFeedVersionPrefix) else { + guard let version = d[Key.version] as? String, let _ = version.range(of: JSONFeedParser.jsonFeedVersionMarker) else { throw FeedParserError(.jsonFeedVersionNotFound) } guard let itemsArray = d[Key.items] as? JSONArray else { diff --git a/Frameworks/RSParser/RSParser.xcodeproj/project.pbxproj b/Frameworks/RSParser/RSParser.xcodeproj/project.pbxproj index 21da20431..ab1ffab79 100644 --- a/Frameworks/RSParser/RSParser.xcodeproj/project.pbxproj +++ b/Frameworks/RSParser/RSParser.xcodeproj/project.pbxproj @@ -95,6 +95,7 @@ 849A03EA1F01F92B00122600 /* inessential.json in Resources */ = {isa = PBXBuildFile; fileRef = 849A03E91F01F92B00122600 /* inessential.json */; }; 849A03EC1F01FCDC00122600 /* RSSInJSONParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A03EB1F01FCDC00122600 /* RSSInJSONParserTests.swift */; }; 84B19A771FDA438300458981 /* natasha.xml in Resources */ = {isa = PBXBuildFile; fileRef = 84B19A761FDA438300458981 /* natasha.xml */; }; + 84CF85BB2035455B0096F368 /* pxlnv.json in Resources */ = {isa = PBXBuildFile; fileRef = 84CF85BA2035455B0096F368 /* pxlnv.json */; }; 84D81BDC1EFA28E700652332 /* RSParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 84D81BDA1EFA28E700652332 /* RSParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84D81BDE1EFA2B7D00652332 /* ParsedFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D81BDD1EFA2B7D00652332 /* ParsedFeed.swift */; }; 84D81BE01EFA2BAE00652332 /* FeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D81BDF1EFA2BAE00652332 /* FeedType.swift */; }; @@ -209,6 +210,7 @@ 849A03E91F01F92B00122600 /* inessential.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = inessential.json; sourceTree = ""; }; 849A03EB1F01FCDC00122600 /* RSSInJSONParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RSSInJSONParserTests.swift; sourceTree = ""; }; 84B19A761FDA438300458981 /* natasha.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = natasha.xml; sourceTree = ""; }; + 84CF85BA2035455B0096F368 /* pxlnv.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pxlnv.json; sourceTree = ""; }; 84D81BD91EFA28E700652332 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84D81BDA1EFA28E700652332 /* RSParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RSParser.h; sourceTree = ""; }; 84D81BDD1EFA2B7D00652332 /* ParsedFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ParsedFeed.swift; path = Feeds/ParsedFeed.swift; sourceTree = ""; }; @@ -412,6 +414,7 @@ 8401FF831FE87C2E0080F13F /* theomnishow.rss */, 844B5B3D1FE9A13B00C7C76A /* 4fsodonline.atom */, 844B5B3F1FE9A45200C7C76A /* expertopinionent.atom */, + 84CF85BA2035455B0096F368 /* pxlnv.json */, 849A03CF1F0081EA00122600 /* Subs.opml */, ); path = Resources; @@ -565,10 +568,12 @@ 84FF5F831EFA285800C15A01 = { CreatedOnToolsVersion = 9.0; LastSwiftMigration = 0900; + ProvisioningStyle = Automatic; }; 84FF5F8C1EFA285800C15A01 = { CreatedOnToolsVersion = 9.0; LastSwiftMigration = 0900; + ProvisioningStyle = Automatic; }; }; }; @@ -624,6 +629,7 @@ 84DA2E21200415D500A4D03B /* curt.json in Resources */, 849A03D31F0081EA00122600 /* furbo.html in Resources */, 849A03E81F01F88600122600 /* ScriptingNews.json in Resources */, + 84CF85BB2035455B0096F368 /* pxlnv.json in Resources */, 844B5B3E1FE9A13C00C7C76A /* 4fsodonline.atom in Resources */, 840FDCB81F0218670041F61B /* DaringFireball.atom in Resources */, 849A03D91F0081EA00122600 /* sixcolors.html in Resources */, diff --git a/Frameworks/RSParser/RSParserTests/FeedParserTypeTests.swift b/Frameworks/RSParser/RSParserTests/FeedParserTypeTests.swift index 09705ae3e..e7bbb124c 100644 --- a/Frameworks/RSParser/RSParserTests/FeedParserTypeTests.swift +++ b/Frameworks/RSParser/RSParserTests/FeedParserTypeTests.swift @@ -141,6 +141,13 @@ class FeedParserTypeTests: XCTestCase { XCTAssertTrue(type == .jsonFeed) } + func testPixelEnvyJSONFeedType() { + + let d = parserData("pxlnv", "json", "http://pxlnv.com/") + let type = feedType(d) + XCTAssertTrue(type == .jsonFeed) + } + // MARK: Unknown func testPartialAllThisUnknownFeedType() { diff --git a/Frameworks/RSParser/RSParserTests/JSONFeedParserTests.swift b/Frameworks/RSParser/RSParserTests/JSONFeedParserTests.swift index f7c5e61eb..c68cdc260 100644 --- a/Frameworks/RSParser/RSParserTests/JSONFeedParserTests.swift +++ b/Frameworks/RSParser/RSParserTests/JSONFeedParserTests.swift @@ -84,4 +84,12 @@ class JSONFeedParserTests: XCTestCase { XCTAssertTrue(didFindTwitterQuitterArticle) } + + func testPixelEnvy() { + + let d = parserData("pxlnv", "json", "http://pxlnv.com/") + let parsedFeed = try! FeedParser.parse(d)! + XCTAssertEqual(parsedFeed.items.count, 20) + + } } diff --git a/Frameworks/RSParser/RSParserTests/Resources/pxlnv.json b/Frameworks/RSParser/RSParserTests/Resources/pxlnv.json new file mode 100644 index 000000000..299c50cae --- /dev/null +++ b/Frameworks/RSParser/RSParserTests/Resources/pxlnv.json @@ -0,0 +1,249 @@ +{ + "version": "http://jsonfeed.org/version/1", + "user_comment": "This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL -- https://pxlnv.com/feed/json/ -- and add it your reader.", + "home_page_url": "https://pxlnv.com", + "feed_url": "https://pxlnv.com/feed/json/", + "title": "Pixel Envy", + "description": "", + "items": [ + { + "id": "https://pxlnv.com/linklog/uber-losses-2017/", + "url": "https://pxlnv.com/linklog/uber-losses-2017/", + "external_url": "https://www.bloomberg.com/news/articles/2018-02-13/uber-sales-reach-7-5-billion-in-2017-despite-persistent-turmoil", + "title": "Uber Lost $4.5 Billion in 2017", + "content_html": "

Eric Newcomer, Bloomberg:

\n\n
\n

Adjusted net revenue last quarter increased 61 percent to $2.22 billion from the same period in 2016. Meanwhile, the total value of fares grew to $11 billion that quarter. It was the first full quarter under Dara Khosrowshahi, who took over the troubled business in September.

\n \n

Despite a turbulent year for the ride-hailing company, sales were $7.5 billion. But the company also posted a substantial loss of $4.5 billion. There are few historical precedents for the scale of its loss.

\n
\n\n

In 2016, Pixel Envy earned $3 billion more than Uber, and I’m thrilled to report that the delta between me and Uber for 2017 was 50% greater.

\n\n

A reminder that no taxi company could survive losses like those Uber has been posting; also, that the reason a fare with an Uber driver is cheaper is because it’s subsidized at below-market rates by venture capital firms; and that, despite some benefits for gig economy workers in the new tax code, Uber is among many gig-type companies that does not provide health coverage for their American drivers.

\n

\n", + "date_published": "2018-02-13T23:23:12+00:00", + "date_modified": "2018-02-13T23:51:35+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/facebook-vpn-protect/", + "url": "https://pxlnv.com/linklog/facebook-vpn-protect/", + "external_url": "https://techcrunch.com/2018/02/12/facebook-starts-pushing-its-data-tracking-onavo-vpn-within-its-main-mobile-app/", + "title": "Under the Guise of Security, Facebook is Promoting Their VPN in Their iOS App", + "content_html": "

Sarah Perez, TechCrunch:

\n\n
\n

Marketing Onavo within Facebook itself could lead to a boost in users for the VPN app, which promises to warn users of malicious websites and keep information secure \u2013 like bank account and credit card numbers \u2013 as you browse. But Facebook didn\u2019t buy Onavo for its security protections.

\n \n

Instead, Onavo\u2019s VPN allow Facebook to monitor user activity across apps, giving Facebook a big advantage in terms of spotting new trends across the larger mobile ecosystem. For example, Facebook gets an early heads up about apps that\u00a0are becoming breakout hits; it can tell which are seeing slowing user growth; it sees which apps\u2019 new features appear to be resonating with their users, and much more.

\n \n

This data has already helped Facebook in a number of ways, most notably in its battle with Snapchat. At The WSJ reported last August, Facebook could tell that Instagram\u2019s launch of Stories \u2013 a Snapchat-like feature \u2013 was working to slow Snapchat\u2019s user growth, before the company itself even publicly disclosed this fact.

\n
\n\n

Think about that: Facebook has one of the largest platforms in the world, and is using that influence to promote a service that they control to spot and preemptively eliminate potential competitors. The reason they\u2019re able to do all of these things is because of their size and dominance.

\n\n

I understand the reluctance by many regulators and industry observers to say that Facebook ought to be broken up into smaller, unaffiliated companies, but I\u2019m struggling to see many other ways to keep the company\u2019s influence in check. Largely ignoring it, as has been done so far, is bad for competition. Even if you ignore potential anticompetitive issues, there\u2019s still a question of whether users of Facebook\u2019s VPN are adequately aware of how the company accessed and uses their data.

\n

\n", + "date_published": "2018-02-13T14:05:20+00:00", + "date_modified": "2018-02-13T14:33:10+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/amp-for-email/", + "url": "https://pxlnv.com/linklog/amp-for-email/", + "external_url": "https://gsuite-developers.googleblog.com/2018/02/AMP-for-email-developer-preview.html?m=1", + "title": "Google Announces AMP For Email Spec", + "content_html": "

Gmail engineer Raymond Wainman:

\n\n
\n

You may have heard of the open-source framework, Accelerated Mobile Pages\u00a0(AMP). It\u2019s a framework for developers to create faster-loading mobile content on the web. Beyond simply loading pages faster, AMP now supports building a wide range of rich pages for the web. Today, we\u2019re announcing AMP for Email so that emails can be formatted and sent as AMP documents. As a part of this, we\u2019re also kicking off the Gmail Developer Preview of AMP for Email \u2014 so once you\u2019ve built your emails, you\u2019ll be able to test them in Gmail.

\n
\n\n

Not content with bifurcating the web with the introduction of a proprietary HTML-like webpage format, Google is now trying to split email clients into Gmail and everybody else. Gmail is already an email-like product and has some of the worst CSS support of mainstream email clients.

\n\n

Of course, there\u2019s a good chance the advanced capabilities of this format won\u2019t catch on because email clients are already pretty fragmented as things stand today. It\u2019s an area of the web where the lowest common denominators \u2014 HTML tables and old-school tags like <font> \u2014 are used with disturbing regularity, simply because it\u2019s the only markup that works well in all clients. It\u2019s frustrating enough to build emails as things are; I imagine many developers will reject this because it adds yet another layer of complexity to their workflow that may not be used by a large number of recipients.

\n\n

Developers shouldn\u2019t reject this on those grounds alone, however. Google\u2019s increasing demands to bend open formats with proprietary variations is a fantastic reason to avoid AMP in email messages.

\n

\n", + "date_published": "2018-02-13T12:14:16+00:00", + "date_modified": "2018-02-13T12:21:35+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/gurman-apple-development-cycle/", + "url": "https://pxlnv.com/linklog/gurman-apple-development-cycle/", + "external_url": "https://www.bloomberg.com/news/articles/2018-02-12/how-apple-plans-to-root-out-bugs-revamp-iphone-software", + "title": "Apple Reportedly Focusing Less on Monolithic Annual iOS Updates", + "content_html": "

Mark Gurman, Bloomberg:

\n\n
\n

Apple’s annual software upgrade this fall will offer users plenty of new features: enabling a single set of apps to work across iPhones, iPads and Macs, a Digital Health tool to show parents how much time their children have been staring at their screen and improvements to Animojis, those cartoon characters controlled by the iPhone X’s facial recognition sensor.

\n \n

But just as important this year will be what Apple doesn’t introduce: redesigned home screens for the iPhone, iPad and CarPlay, and a revamped Photos app that can suggest which images to view.

\n \n

These features were delayed after Apple Inc. concluded it needed its own major upgrade in the way the company develops\u00a0and introduces new products. Instead of keeping engineers on a relentless annual schedule and cramming features into a single update, Apple will start focusing\u00a0on the next two years of updates\u00a0for its iPhone and iPad operating system, according to people familiar with the change.\u00a0The company will continue to update its software annually,\u00a0but internally engineers will have more discretion to push back features that aren’t as polished to the following year.\u00a0

\n
\n\n

The biggest news here is that Apple is reportedly adjusting their internal processes to try to reduce the demands of an annual update. But I\u2019m not sure how much will change externally because this sounds a lot like the way they presently release iOS updates: still a focus on new features in the autumn, with some features debuting later in that major version\u2019s release cycle. Apple Pay Cash, for instance, was announced at WWDC in June with the implication that it would be release with iOS 11.0, but it wasn\u2019t launched until November with iOS 11.2.

\n\n

If the changes are as modest as this report makes them out to be, how much of an improvement can we realistically expect in software quality?

\n

\n", + "date_published": "2018-02-12T23:35:02+00:00", + "date_modified": "2018-02-12T23:35:02+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/autocorrect-contacts-apps/", + "url": "https://pxlnv.com/linklog/autocorrect-contacts-apps/", + "external_url": "https://twitter.com/wilshipley/status/960309688599375872", + "title": "Autocorrect Based on Contacts and Apps", + "content_html": "

Wil Shipley:

\n\n
\n

Imagine being in charge of an algorithm that hundreds of millions of users depend on every day and saying, \u201cHey, let\u2019s take any word that\u2019s capitalized in your contacts and just always capitalize it in text messages!\u201d

\n
\n\n

It\u2019s not just contact names that inform the autocorrect dictionary: any capitalized word in a contact record will be fed into the dictionary, as will installed apps. So, if you know someone who works at, say, Apple, or you have the Transit app installed, you will find yourself regularly undoing the automatic capitalization of those words when talking about fruit or the very concept of public transit. Sometimes, autocorrect will fix its aggressive capitalization after it is given more context by typing several more words; but, frequently, it does not.

\n

\n", + "date_published": "2018-02-12T13:12:07+00:00", + "date_modified": "2018-02-12T13:14:11+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/browsealoud-cryptojacking/", + "url": "https://pxlnv.com/linklog/browsealoud-cryptojacking/", + "external_url": "https://scotthelme.co.uk/protect-site-from-cryptojacking-csp-sri/", + "title": "A Third-Party Script Used by Government Websites Was Compromised to Mine Cryptocurrency", + "content_html": "

Scott Helme:

\n\n
\n

I had a friend of mine get in touch about his AV program throwing a warning when visiting the ICO website. The ICO bill themselves as:

\n \n
\n

The UK\u2019s independent authority set up to uphold information rights in the public interest, promoting openness by public bodies and data privacy for individuals.

\n
\n \n

They’re the people we complain to when companies do bad things with our data. It was pretty alarming to realise that they were running a crypto miner on their site, their whole site, every single page.

\n \n

At first the obvious thought is that the ICO were compromised so I immediately started digging into this after firing off a few emails to contact people who may be able to help me with disclosure. I quickly realised though that this script, whilst present on the ICO website, was not being hosted by the ICO, it was included by a 3rd party library they loaded.

\n
\n\n

Scary as it is, this is arguably relatively minor incident; imagine if it were a more malicious script \u2014 something like a keylogger. It would be wise for web developers reliant upon third-party scripts to treat them as though they will, at some point, carry malware.

\n

\n", + "date_published": "2018-02-12T13:06:34+00:00", + "date_modified": "2018-02-12T13:06:34+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/equifax-lost-more-data/", + "url": "https://pxlnv.com/linklog/equifax-lost-more-data/", + "external_url": "http://www.zdnet.com/article/hackers-stole-more-equifax-data-than-first-thought/", + "title": "Equifax Continues to Be Useless and Terrible at Absolutely Everything", + "content_html": "

Zack Whittaker, ZDNet:

\n\n
\n

Hackers stole more data from Equifax in a breach last year than initially thought.

\n \n

[\u2026]

\n \n

A letter published Friday by committee member Sen. Elizabeth Warren (D-MA) to acting Equifax chief executive Paulino do Rego Barros summarized the senator’s five-month investigation into the Equifax breach, which said tax identification numbers (TINs), email addresses, and additional license information \u2014 such as issue dates and by which state \u2014 were not originally disclosed.

\n
\n\n

A reminder that Reuters reported earlier this month that the CFPB investigation into the Equifax breach is \u201con ice\u201d.

\n

\n", + "date_published": "2018-02-10T20:47:04+00:00", + "date_modified": "2018-02-10T20:47:04+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/intern-iphone-source-code/", + "url": "https://pxlnv.com/linklog/intern-iphone-source-code/", + "external_url": "https://motherboard.vice.com/en_us/article/xw5yd7/how-iphone-iboot-source-code-leaked-on-github", + "title": "An Apple Intern Reportedly Stole iOS Source Code and Leaked It to His Friends", + "content_html": "

Lorenzo Franceschi-Bicchierai, Vice:

\n\n
\n

According to these sources, the person who stole the code didn\u2019t have an axe to grind with Apple. Instead, while working at Apple, friends of the employee encouraged the worker to leak internal Apple code. Those friends were in the jailbreaking community and wanted the source code for their security research.

\n \n

The person took the iBoot source code\u2014and additional code that has yet to be widely leaked\u2014and shared it with a small group of five people.

\n \n

\u201cHe pulled everything, all sorts of Apple internal tools and whatnot,\u201d a friend of the intern told me. Motherboard saw screenshots of additional source code and file names that were not included in the GitHub leak and were dated from around the time of this first leak.

\n
\n\n

Baseband code from the same time period has also been leaked publicly.

\n

\n", + "date_published": "2018-02-10T18:10:23+00:00", + "date_modified": "2018-02-10T18:10:40+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/everything-easy-is-hard-again/", + "url": "https://pxlnv.com/linklog/everything-easy-is-hard-again/", + "external_url": "https://frankchimero.com/writing/everything-easy-is-hard-again/", + "title": "Everything Easy is Hard Again", + "content_html": "

Frank Chimero:

\n\n
\n

If you go talk to a senior software developer, you\u2019ll probably hear them complain about spaghetti code. This is when code is overwrought, unorganized, opaque, and snarled with dependencies. I perked up when I heard the term used for the first time, because, while I can\u2019t identify spaghetti code as a designer, I sure as hell know about spaghetti workflows and spaghetti toolchains. It feels like we\u2019re there now on the web.

\n \n

[\u2026]

\n \n

I wonder what young designers think of this situation and how they are educating themselves in a complicated field. How do they learn if the code is illegible? Does it seem like more experienced people are pulling up the ladder of opportunity by doing this? Twenty years ago, I decided to make my own website, because I saw an example of HTML and I could read it. Many of my design peers are the same. We possess skills to make websites, but we stopped there. We stuck with markup and never progressed into full-on programming, because we were only willing to go as far as things were legible.

\n
\n\n

This essay resonated deeply with me. I wrote my first line of HTML about twenty years ago. I remember editing the Yahoo homepage in Netscape Composer around that time, and building a Geocities website not that long after. It felt easy and approachable, even if <table> syntax was often inscrutable and unpredictable. A few years later, the CSS wave hit the web and I learned about why it was appropriate to separate presentational code from the page’s markup.1 CSS has become more complicated since then, but it continues to make sense to me, even though I need to look up the flexbox syntax every time I use it.

\n\n

Over the last five years or so, even the most basic website stopped being treated as a collection of documents and started being thought of as software. Over the same period of time, I have gone from thinking that I know how to build a website quickly and efficiently to having absolutely no clue where to start learning about any of this stuff. I can’t imagine being eight years old again and being interested in the web as something anyone can contribute to.

\n\n

See Also: Chimero’s spoken, longer-form version of this essay, given as a talk at Mirror Conf.

\n\n
\n
\n
    \n\n
  1. \n

    And, yet, the easiest way to make a few boxes side-by-side that have the same resulting height despite allowing a flexible amount of text in each remains display: table-cell. The same technique allows perhaps the easiest way to vertically centre an unpredictable amount of text. Like tables for layout purposes, it still isn’t semantically correct, but we use it anyway. ↩︎

    \n
  2. \n\n
\n
\n

\n", + "date_published": "2018-02-10T17:31:43+00:00", + "date_modified": "2018-02-10T17:46:35+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/blog/googles-design-prowess/", + "url": "https://pxlnv.com/blog/googles-design-prowess/", + "title": "Reports of Google\u2019s Newfound Design Prowess Have Been Greatly Exaggerated", + "content_html": "

There is something unique about deliberately contrarian-for-the-sake-of-being-contrarian positions that irks me so much, and I’m not sure what it is. I don’t know that it’s because these arguments are poor so much as it is that they’re easily shown to be poor. Maybe it’s the author’s optimism that convinces them that their piece is worth publishing, or maybe it’s just provocative for its own sake \u2014 the latter of which is even more irritating for me because I know that my frustration with the argument is entirely the author’s intention, and I’d rather not play into that. Whatever the case, it’s the sort of thing that rattles around inside my head.

\n\n

Which brings me to two pieces written by Joshua Topolsky last autumn. The first, \u201cApple is Really Bad at Design\u201d, posits that Apple’s recent products no longer represent the pinnacle of design in the industry. To be fair to Topolsky, he may sincerely believe that there’s value in challenging the assumption that these products are well-designed, and I think that’s completely reasonable. It’s that article’s companion piece, \u201cGoogle is Really Good at Design\u201d, that occasionally creeps up in my mind.

\n\n

Topolsky:

\n\n
\n

The concepts inherent in Material Design \u2014 a system of literal layers that evoke the tactility of a stack of paper, but offers the flexibility of digital spaces; a responsive layout concept that assumes no two devices may be exactly the same size or shape; a bold use of typography, motion, and color \u2014 showcase a decidedly different approach than Apple has taken. Where Jony Ive and company have produced a scattered, visually unmoored solution that seems to be solving small problems bite-by-bite, Google essentially blew up what had come before and reset. This radical rethink has spread into Google’s deep web pockets, meaning that a logical system of navigation and connectivity not only informs what you see on your phone when you interact with apps and services, but what you get on the web, on a laptop, or on a TV. Gmail is Gmail is Gmail, responding to whatever screen it\u2019s on. And sometimes, thanks to Google\u2019s deep machine learning and natural language chops, Gmail is also the disembodied voice you talk to while you\u2019re driving. In Google\u2019s universe, its voice-activated Assistant isn\u2019t middleware \u2014 it\u2019s everyware, tapping deeply and natively into all of the company\u2019s nodes.

\n
\n\n

Topolsky is generally right in saying that Google’s approach to user interfaces is remarkably consistent across everything, but I would argue that it represents why their products are often so frustrating and cumbersome to use.

\n\n

Case in point: their new YouTube app for tvOS. The last version didn’t represent a dramatic design statement or look particularly special \u2014 it was pretty much the same as any of the default tvOS apps \u2014 but it worked, for the most part. It was the only app I’ve used on my Apple TV that would regularly kick me back to the tvOS home screen instead of the last screen in the app when I pressed the remote’s menu button while watching a video, and it had stability problems when searching, but it wasn’t terrible.

\n\n

The new app, though, represents everything wrong with Google’s present UI design philosophy. It follows virtually none of the Apple TV platform conventions:

\n\n
    \n
  • There’s a sidebar on the left that looks like an Android action bar.

  • \n
  • Swiping to the left on the touch pad from any of the app’s menu screens will open a main menu panel, with navigation options for your subscriptions, video history, and own video library.

  • \n
  • There’s also a horizontal navigation element, similar to the type that you would find in a default tvOS app.

  • \n
  • None of these elements behaves as you might expect, primarily because the YouTube app doesn’t interpret swipes and scrolls like any other app. There’s no audible blip whenever you select something, and swiping around manages to be both sluggish and jerky.

    \n\n

    The frustratingly slow scrolling is especially pronounced on the aforementioned horizontal navigation element because swiping just a little too far to the left will open the modal main menu panel that covers a third of the screen.

    \n\n

    The slow scrolling is also apparent in the main menu panel. The scrolling “friction”, for lack of a better term, is such that swiping down just a little is unlikely to have any effect, and swiping down just a little bit more will move the selector down two menu items. It can be very difficult to get it to move one menu item at a time.

  • \n
  • There’s no sense of transition between screens or states. Instead of fading, screens simply change; instead of smoothly sliding left or right when scrolling across thumbnails, there will often be a sudden jump to load the new set of thumbnails.

  • \n
  • Swiping horizontally across the remote while a video is playing will scrub the video. This is something Apple quickly changed after the fourth-generation Apple TV debuted because of how easy it was to accidentally invoke it.

  • \n
  • Tapping on the remote’s touch pad to display onscreen controls automatically selects the play/pause button instead of the scrubber, as in other tvOS apps, and there are two levels of controls in the custom player.

  • \n
  • The app is also an ugly sea of mid-tone greys.

  • \n
\n\n

It isn’t unheard-of for an Apple TV app from a major third party to fail to adhere to platform conventions. The Amazon Prime app doesn’t look or behave anything like a native app because it’s basically a web app. Hulu and Netflix also have some pretty crappy apps that don’t really function like a tvOS app ought to.

\n\n

But this also isn’t unlike Google, which has completely disregarded platform standards with their major iOS apps for years. There’s nothing wrong with making apps of a particular style \u2014 my favourite developers all have their unique quirks and styles that help identify their apps as theirs \u2014 but Google’s apps frequently feel less like they’re trying to create branded iOS apps and more like they want their Android apps to run on iOS.

\n\n

This isn’t a new argument, and Google has become a moderately better citizen on iOS over the past couple of years: their sharing glyph now looks like the system standard one instead of lazily copying the shape they use on Android, for example. This new YouTube app for tvOS is a step back, however. It feels like a half-assed port. When there’s no clear effort by a huge company like Google to even try to make their products fit a different platform, it indicates a lack of care and attention to detail. It also demonstrates that users’ expectations and learned behaviours are less important than self-promotion and branding.

\n\n

What it shows, ultimately, is a lack of consideration for design.

\n", + "date_published": "2018-02-08T00:14:37+00:00", + "date_modified": "2018-02-08T10:24:41+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/facebook-flattening/", + "url": "https://pxlnv.com/linklog/facebook-flattening/", + "external_url": "http://splitsider.com/2018/02/how-facebook-is-killing-comedy/", + "title": "The Facebook Flattening", + "content_html": "

Matt Klinman of Funny or Die, in an interview with Sarah Aswell of Splitsider on the effect of Facebook\u2019s algorithmic timeline changes on independent media:

\n\n
\n

This writer John Herrman writes about this a lot \u2014 he used to write for The Awl, rest in peace \u2014 he talks about how Facebook flattens everything out and makes it the same. That\u2019s how we have a Russian propaganda problem. An article from something like, I don\u2019t know, Rebel Patriot News written by a Macedonian teen or something looks exactly the same as a New York Times article. It\u2019s the same for comedy websites. There\u2019s a reason that Mad magazine looks different from Vanity Fair. They need to convey a different aesthetic and a different tone for their content to really pop. Facebook is the great de-contextualizer. There\u2019s no more feeling of jumping into a whole new world on the internet anymore \u2014 everything looks exactly the same.

\n
\n\n

The premise of this piece is that \u201cFacebook is killing comedy\u201d \u2014 Funny or Die had to lay off a bunch of writers because of reduced traffic from Facebook. I\u2019ve written about that before because, while I think websites like Funny or Die should be less dependent on traffic from any one source, but Facebook is not entirely blameless either.

\n\n

This pullquote, though, is one of the best encapsulations I\u2019ve seen of the effects of Facebook\u2019s ecosystem, particularly its ability to erase context.

\n

\n", + "date_published": "2018-02-07T15:15:41+00:00", + "date_modified": "2018-02-07T15:19:06+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/everyone-creates/", + "url": "https://pxlnv.com/linklog/everyone-creates/", + "external_url": "https://www.everyonecreates.org/", + "title": "On the Internet, Everyone is a Creator", + "content_html": "

A fantastic new site from the Copia Institute, with stories from artists empowered by the internet and reasonable intellectual property law.

\n

\n", + "date_published": "2018-02-07T10:49:56+00:00", + "date_modified": "2018-02-07T11:00:21+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/fcc-verizon-jokes/", + "url": "https://pxlnv.com/linklog/fcc-verizon-jokes/", + "external_url": "https://gizmodo.com/fcc-says-releasing-jokes-it-wrote-about-ajit-pai-collud-1822763256?utm_campaign=socialflow_gizmodo_twitter&utm_source=gizmodo_twitter&utm_medium=socialflow", + "title": "FCC Says Releasing ‘Jokes’ It Wrote About Ajit Pai Colluding With Verizon Would ‘Harm’ Agency", + "content_html": "

Dell Cameron, Gizmodo:

\n\n
\n

At its own discretion, the Federal Communications Commission has chosen to block the release of records related to a video produced last year in which FCC Chairman Ajit Pai and a Verizon executive joke about installing a \u201cVerizon puppet\u201d as head of the FCC.

\n \n

In a letter to Gizmodo last week, the agency said it was withholding the records from the public in order to prevent harm to the agency \u2014 an excuse experts say is a flagrant attempt to skirt federal transparency law.

\n
\n\n

I\u2019m not certain internal records are required to damage the agency\u2019s reputation these days.

\n

\n", + "date_published": "2018-02-06T13:44:40+00:00", + "date_modified": "2018-02-06T13:44:40+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/apple-music-long-game/", + "url": "https://pxlnv.com/linklog/apple-music-long-game/", + "external_url": "https://www.kirkville.com/apple-music-now-has-36-million-subscribers-could-eclipse-spotify-in-united-states-this-year-mac-rumors/", + "title": "The Apple Music Long Game", + "content_html": "

Kirk McElhearn:

\n\n
\n

As streaming takes over from buying music, what\u2019s the endgame? If Apple rolls in a major video offering \u2013 either as part of the Apple Music service, or as an add-on \u2013 then will Spotify be bought out by, say, Netflix? Amazon already has both, and there probably won\u2019t be room for more than two or three players in that market.

\n
\n\n

Netflix doesn\u2019t offer a free tier. Why would Apple offer one with a subscription to streaming music \u2014 and so far, at least \u2014 original video programming?

\n

\n", + "date_published": "2018-02-06T13:33:32+00:00", + "date_modified": "2018-02-06T13:33:32+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/apple-search-engine/", + "url": "https://pxlnv.com/linklog/apple-search-engine/", + "external_url": "https://www.appleworld.today/blog/2018/2/5/will-apple-ever-make-its-own-search-engine-apple-search-anyone", + "title": "Apple\u2019s Mysterious Search Engine Already Exists", + "content_html": "

Something fishy is going on in the world of Apple-centric websites. Yesterday, I posted a link to a silly piece arguing that Apple Music needs a free tier. Today, Dennis Sellers of Apple World Today is surprised by the idea that Apple might be working on a search engine:

\n\n
\n

A couple of years ago, Apple posted a listing to its Jobs at Apple page describing an engineering project manager position for “Apple Search.” Could the company could be working on a full-fledged search engine for use on macOS and iOS platforms?

\n
\n\n

This already exists. It\u2019s built into Spotlight on MacOS and the iOS search function that used to be called Spotlight. It\u2019s also baked into Safari and Siri, the latter of which Sellers notes in his article.

\n\n

It\u2019s almost like both of these pieces were written by people completely unfamiliar with Apple\u2019s ecosystem. Maybe I\u2019m wrong \u2014 maybe I\u2019m just being cocky, and Apple is working on a rival to Google.com. Maybe I\u2019m completely misguided here. But I don\u2019t think so; both of these articles seem pretty boneheaded.

\n

\n", + "date_published": "2018-02-06T13:21:21+00:00", + "date_modified": "2018-02-06T13:21:39+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/homepod-review-roundup/", + "url": "https://pxlnv.com/linklog/homepod-review-roundup/", + "external_url": "https://techcrunch.com/2018/02/06/a-four-sentence-homepod-review-with-appendices/", + "title": "HomePod Review Roundup", + "content_html": "

Reviews of the HomePod are going live across the web this morning ahead of its release this Friday, and it seems like it’s living up to what was promised: a very good speaker with extraordinary audio engineering and limited Siri capabilities.

\n\n

Nicole Nguyen, Buzzfeed:

\n\n
\n

[Kate Bergeron, vice president of hardware engineering,] was speaking to a small group of tech bloggers, including myself, last Monday in Apple\u2019s Cupertino, CA-based audio lab, just minutes from the new Apple Park spaceship campus. About six years ago, according to Bergeron, the company began working on HomePod by attempting to answer this question: \u201cWhat if we decided to design a loudspeaker that we could put in any room, and it wouldn\u2019t affect the sound?\u201d

\n \n

This question is very different from the question the Amazon Echo and Google Home are trying to address. Those speakers\u2019 primary aim is to offer hands-free help, by way of turning on the lights in the living room, telling you what traffic to work is like, setting timers, and playing podcasts while you\u2019re busy cooking breakfast.

\n
\n\n

Matthew Panzarino, TechCrunch:

\n\n
\n

The sound that comes from the HomePod can best be described as precise. It\u2019s not as loud as some others like Google Home Max or as bright (and versatile) as the Sonos Play 1, but it destroys the muddy sound of less sophisticated options like the Amazon Echo. To genuinely fill a large room you need two but anyone in a small house or apartment will get great sound from one.

\n \n

[\u2026]

\n \n

While you can send texts and take notes and set reminders and handle phone calls begun on your iPhone, that\u2019s about all of the extracurriculars and they\u2019re all focused on single-user experiences. If you\u2019re logged in to your iCloud account, all of the messages and calls are yours and come from you. That\u2019s great if you\u2019re a single dude living alone, but it completely falls apart in a family environment. Apple allows you to toggle these options off as the iCloud account owner and I recommend you do before it all ends in tears. Unless you live alone in which case Mazel, it sounds peaceful.

\n
\n\n

Joanna Stern, Wall Street Journal:

\n\n
\n

There are other problems I won\u2019t shut up about: Many people will put a HomePod in the kitchen, yet it can\u2019t set two simultaneous cooking timers. It can\u2019t wake me up to \u201cWake Me Up Before You Go-Go,\u201d either. Echo and Google Home can do both. Apple says it is improving Siri all the time.

\n \n

[\u2026]

\n \n

Siri turns out to be quite a good butler. Through the Home app, you can set up various HomeKit-compatible smart-home devices, and the voice prompts to control them. With Philips Hue lightbulbs and three iHome smart plugs, I was quickly commanding Siri to change my nightlight to a fuchsia hue, make tea via my electric kettle and turn on the humidifier.

\n
\n\n

Brian X. Chen:

\n\n
\n

Most bizarre thing about HomePod: It didn\u2019t play music relevant to my listening history or prefs when asked \u201cHey Siri, Play some music.\u201d

\n \n

Siri should be better on HomePod because it\u2019s the primary way to control it. But yeah, it\u2019s worse.

\n
\n\n

I don’t think it’s a mistake to question whether Siri’s lacklustre abilities will be a hindrance to the success of the HomePod. Apple may be positioning it as a great speaker first and a smart speaker second, and the market will get to tell them whether that’s a reasonable way to judge the product. And, perhaps, people will love it for a speaker alone \u2014 it’s clearly a very good one. The more damning thing to consider about Siri is not that it is poor on the HomePod, but that it is poor everywhere. Fortunately, software can be updated, so that just means that we need to see some commitment from Apple that Siri is a high priority.

\n

\n", + "date_published": "2018-02-06T07:44:08+00:00", + "date_modified": "2018-02-06T07:44:22+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/pai-cites-obama-broadband-investments/", + "url": "https://pxlnv.com/linklog/pai-cites-obama-broadband-investments/", + "external_url": "https://arstechnica.com/information-technology/2018/02/heres-ajit-pais-proof-that-killing-net-neutrality-created-more-broadband/", + "title": "Ajit Pai\u2019s FCC Cites Obama-Era Broadband Investments", + "content_html": "

Stop me if you\u2019ve heard this one before, but an assessment made based on the actions of the current American administration has been undermined by their complete lack of scruples.

\n\n

Crazy, I know.

\n\n

Earlier this year, the FCC voted to retain a faster definition of broadband established by the previous administration. As far as I could tell, the defeated proposal was simply a way to broaden the definition of broadband and give the impression in reports that access to broadband had improved for Americans without doing the work of actually, you know, investing in better networks. After it was voted down, I figured that this FCC administration would, at least, avoid resorting to ridiculous tactics to gain the impression of a policy win without any actually good policy. But I should have known better.

\n\n

Jon Brodkin, Ars Technica:

\n\n
\n

Anyone who is familiar with the FCC chairman’s rhetoric over the past few years could make two safe predictions about this report. The report would conclude that broadband deployment in the US is going just fine and that the repeal of net neutrality rules is largely responsible for any new broadband deployment.

\n \n

But the FCC’s actual data\u2014based on the extensive Form 477 data submissions Internet service providers must make on a regular basis\u2014only covers broadband deployments through December 2016. Pai wasn’t elevated from commissioner to chairman until January 2017, and he didn’t lead the vote to repeal the net neutrality rules until December 2017. And, technically, those rules are still on the books because the repeal won’t take effect for at least another two months.

\n \n

The timing means that it would be impossible for Pai to present evidence today that broadband deployment is increasing as a result of the net neutrality repeal. But the report claims that’s exactly what happened anyway and says that future data will bear that out. To support its argument, the report claims that broadband deployment projects that were started during the Obama administration were somehow caused by Pai’s deregulatory policies.

\n
\n\n

Not only are they counting Obama-era \u2014 and net neutrality-era \u2014 investment plans as evidence of improved broadband deployment thanks to rules friendly to giant ISPs, they’re also citing past investments that have since been curtailed due to policies implemented by this FCC administration. That’s some bullshit anti-consumer behaviour.

\n

\n", + "date_published": "2018-02-05T23:01:36+00:00", + "date_modified": "2018-02-05T23:02:19+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/apple-music-growing-faster-than-spotify/", + "url": "https://pxlnv.com/linklog/apple-music-growing-faster-than-spotify/", + "external_url": "https://www.macworld.com/article/3252780/streaming-services/apple-music-overtaking-spotify-in-the-u-s-shows-why-it-needs-a-free-tier-more-than-ever.html", + "title": "In the U.S., Apple Music is Growing Faster Than Spotify in Paid Users", + "content_html": "

Michael Simon, Macworld:

\n\n
\n

According to The Wall Street Journal, Apple is on track to overtake Spotify in U.S. paid subscribers, a sign that the three-year-old music service is making serious inroads in a highly competitive landscape. The report states that Apple Music has been gaining U.S. subscribers at a 3 percent higher clip than Spotify, a trend that would give Apple’s music service a higher subscriber rate by the summer, assuming it continues.

\n
\n\n

That\u2019s terrific news for Apple Music, especially considering that it is only available as a paid service. I wouldn\u2019t be surprised if many users are paying more for music now than they have for a long time. You might think \u2014 quite reasonably, I believe \u2014 that this indicates that Apple\u2019s strategy is working well.

\n\n

But not Simon:

\n\n
\n

With a free Apple Music tier, Apple would not only get music fans to flock to its service in droves, it could also use it as a way to advertise HomePod as the best way to listen to Apple Music at home and AirPods as the ultimate on-the-go solution. With quick ads between songs, it would be speaking directly to a captive audience who shares a love for music. Simply put, there’s no better way to advertise.

\n
\n\n

Without trying to predict the future, I don\u2019t think this fits the existing Apple Music strategy. The HomePod\u2019s integration is clearly best with Apple Music, but I\u2019m not sure that\u2019s a reason to provide a free tier; the free trial more aptly demonstrates the advantages of subscribing to Apple Music.

\n\n

More than anything, I think Simon falls into the same trap many others do: Apple isn\u2019t setting out to build the biggest user base, but a large paying user base. A free trial accomplishes that goal; a free tier does not.

\n

\n", + "date_published": "2018-02-05T13:45:07+00:00", + "date_modified": "2018-02-05T14:28:42+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/cfpb-equifax-investigation-on-ice/", + "url": "https://pxlnv.com/linklog/cfpb-equifax-investigation-on-ice/", + "external_url": "https://www.reuters.com/article/us-usa-equifax-cfpb/exclusive-u-s-consumer-protection-official-puts-equifax-probe-on-ice-sources-idUSKBN1FP0IZ", + "title": "Reuters: CFPB Investigation Into Equifax \u2018Put on Ice\u2019", + "content_html": "

Patrick Rucker, Reuters:

\n\n
\n

The CFPB has the tools to examine a data breach like Equifax, said John Czwartacki, a spokesman, but the agency is not permitted to acknowledge an open investigation. \u201cThe bureau has the desire, expertise, and know-how\u00a0in-house to vigorously pursue hypothetical matters such as these,\u201d he said.

\n \n

Three sources say, though, Mulvaney, the new CFPB chief, has not ordered subpoenas against Equifax or sought sworn testimony from executives, routine steps when launching a full-scale probe. Meanwhile the CFPB has shelved plans for on-the-ground tests of how Equifax protects data, an idea backed by Cordray.

\n \n

The CFPB also recently rebuffed bank regulators at the Federal Reserve, Federal Deposit Insurance Corp and Office of the Comptroller of the Currency when they offered to help with on-site exams of credit bureaus, said two sources familiar with the matter.

\n
\n\n

An investigation of this size and scope will, of course, take lots of time and may not always take a linear direction, but there should never be a question about whether it is proceeding at all. Consumers should never have to wonder whether the Bureau is operating in their best interests, especially given the impact of the Equifax breach on virtually every American adult with a credit card, mortgage, or car.

\n

\n", + "date_published": "2018-02-05T13:21:41+00:00", + "date_modified": "2018-02-10T20:44:24+00:00", + "author": { + "name": "Nick Heer" + } + }, + { + "id": "https://pxlnv.com/linklog/publishers-abandoning-instant-articles/", + "url": "https://pxlnv.com/linklog/publishers-abandoning-instant-articles/", + "external_url": "https://www.cjr.org/tow_center/are-facebook-instant-articles-worth-it.php", + "title": "Major Publishers Are Turning Away From Facebook Instant Articles", + "content_html": "

Pete Brown, Columbia Journalism Review:

\n\n
\n

Of 72 publishers that Facebook identified as original partners in May and October 2015, our analysis of 2,308 links posted to their Facebook pages on January 17, 2018, finds that 38 publications did not post a single Instant Article \u2014 the platform\u2019s fast-loading, native format. In the meantime, Facebook has continued to tout Instant Articles as a success among its journalism efforts. Instant Articles enjoyed rapid expansion in 2017, it says. But if many of the largest reputable outlets are falling out, which publications are driving that growth?

\n
\n\n

Do we think Facebook admits that Google AMP is winning the incredibly dumb race for proprietary news article format, that they keep trying to make Instant Articles work, or that they just give up on news altogether?

\n

\n", + "date_published": "2018-02-02T12:30:55+00:00", + "date_modified": "2018-02-02T12:30:55+00:00", + "author": { + "name": "Nick Heer" + } + } + ] +} \ No newline at end of file diff --git a/Frameworks/RSParser/Utilities/NSData+RSParser.m b/Frameworks/RSParser/Utilities/NSData+RSParser.m index f8a4529c0..f43c8b953 100644 --- a/Frameworks/RSParser/Utilities/NSData+RSParser.m +++ b/Frameworks/RSParser/Utilities/NSData+RSParser.m @@ -38,7 +38,7 @@ static BOOL bytesStartWithRSS(const char *bytes, NSUInteger numberOfBytes); if (![self isProbablyJSON]) { return NO; } - return didFindString("https://jsonfeed.org/version/", self.bytes, self.length) || didFindString("https:\\/\\/jsonfeed.org\\/version\\/", self.bytes, self.length); + return didFindString("://jsonfeed.org/version/", self.bytes, self.length); } - (BOOL)isProbablyRSSInJSON { From d081f041f808a722abdf32d30b03e4c41aa93088 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Thu, 15 Feb 2018 17:50:31 -0800 Subject: [PATCH 28/84] Skip group rows when going to next unread. Fix #273. --- .../Sidebar/SidebarViewController.swift | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift index 07b4004c4..390aefe48 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarViewController.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift @@ -179,7 +179,7 @@ import RSCore func canGoToNextUnread() -> Bool { - if let _ = rowContainingNextUnread() { + if let _ = nextSelectableRowWithUnreadArticle() { return true } return false @@ -187,7 +187,7 @@ import RSCore func goToNextUnread() { - guard let row = rowContainingNextUnread() else { + guard let row = nextSelectableRowWithUnreadArticle() else { assertionFailure("goToNextUnread called before checking if there is a next unread.") return } @@ -380,14 +380,24 @@ private extension SidebarViewController { return false } - func rowContainingNextUnread() -> Int? { - + func rowIsGroupItem(_ row: Int) -> Bool { + + if let node = nodeForRow(row), outlineView.isGroupItem(node) { + return true + } + return false + } + + func nextSelectableRowWithUnreadArticle() -> Int? { + + // Skip group items, because they should never be selected. + let selectedRow = outlineView.selectedRow let numberOfRows = outlineView.numberOfRows var row = selectedRow + 1 while (row < numberOfRows) { - if rowHasAtLeastOneUnreadArticle(row) { + if rowHasAtLeastOneUnreadArticle(row) && !rowIsGroupItem(row) { return row } row += 1 @@ -395,7 +405,7 @@ private extension SidebarViewController { row = 0 while (row <= selectedRow) { - if rowHasAtLeastOneUnreadArticle(row) { + if rowHasAtLeastOneUnreadArticle(row) && !rowIsGroupItem(row) { return row } row += 1 From ec1c49349c15e8adf6576c1b04b48c5cda1f16cd Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Thu, 15 Feb 2018 18:03:24 -0800 Subject: [PATCH 29/84] =?UTF-8?q?Make=20Jason=20Kottke=E2=80=99s=20feed=20?= =?UTF-8?q?a=20default=20feed=20for=20new=20users.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Importers/DefaultFeeds.plist | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Importers/DefaultFeeds.plist b/Importers/DefaultFeeds.plist index 38333e5f2..a3a0d5ec2 100644 --- a/Importers/DefaultFeeds.plist +++ b/Importers/DefaultFeeds.plist @@ -30,11 +30,11 @@ homePageURL - http://www.imore.com/ + https://kottke.org/ editedName - iMore + Jason Kottke url - https://www.imore.com/rss.xml + http://feeds.kottke.org/json homePageURL From 891416e7b70bf98992891eea62087b1fd99512f3 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 16 Feb 2018 13:13:00 -0800 Subject: [PATCH 30/84] Add a special case to the JSON Feed parser for feeds that include HTML entities in their titles. At the moment this is used for kottke.org and pxlnv.com. More could be added later, and these feeds could be removed if fixed. --- .../RSParser/Feeds/JSON/JSONFeedParser.swift | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift b/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift index c77c9dc58..c646c3ffe 100644 --- a/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift +++ b/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift @@ -134,7 +134,7 @@ private extension JSONFeedParser { let url = itemDictionary[Key.url] as? String let externalURL = itemDictionary[Key.externalURL] as? String - let title = itemDictionary[Key.title] as? String + let title = parseTitle(itemDictionary, feedURL) let summary = itemDictionary[Key.summary] as? String let imageURL = itemDictionary[Key.image] as? String let bannerImageURL = itemDictionary[Key.bannerImage] as? String @@ -152,6 +152,34 @@ private extension JSONFeedParser { return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: feedURL, url: url, externalURL: externalURL, title: title, contentHTML: decodedContentHTML, contentText: contentText, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments) } + static func parseTitle(_ itemDictionary: JSONDictionary, _ feedURL: String) -> String? { + + guard let title = itemDictionary[Key.title] as? String else { + return nil + } + + if isSpecialCaseTitleWithEntitiesFeed(feedURL) { + return (title as NSString).rsparser_stringByDecodingHTMLEntities() + } + + return title + } + + static func isSpecialCaseTitleWithEntitiesFeed(_ feedURL: String) -> Bool { + + // As of 16 Feb. 2018, Kottke’s and Heer’s feeds includes HTML entities in the title elements. + // If we find more feeds like this, we’ll add them here. If these feeds get fixed, we’ll remove them. + + let matchStrings = ["kottke.org", "pxlnv.com"] + for matchString in matchStrings { + if feedURL.contains(matchString) { + return true + } + } + + return false + } + static func parseUniqueID(_ itemDictionary: JSONDictionary) -> String? { if let uniqueID = itemDictionary[Key.uniqueID] as? String { From 0e2e0f7eea4a3cbde4401f356ae7a41c1e5785ca Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 16 Feb 2018 13:15:20 -0800 Subject: [PATCH 31/84] Do a case-insensitive match when checking for special-case feed URLs. --- Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift b/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift index c646c3ffe..166898af4 100644 --- a/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift +++ b/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift @@ -170,9 +170,10 @@ private extension JSONFeedParser { // As of 16 Feb. 2018, Kottke’s and Heer’s feeds includes HTML entities in the title elements. // If we find more feeds like this, we’ll add them here. If these feeds get fixed, we’ll remove them. + let lowerFeedURL = feedURL.lowercased() let matchStrings = ["kottke.org", "pxlnv.com"] for matchString in matchStrings { - if feedURL.contains(matchString) { + if lowerFeedURL.contains(matchString) { return true } } From dbab80942083f45781f14ec63fa7aaeb06fe2403 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 16 Feb 2018 21:08:34 -0800 Subject: [PATCH 32/84] Hide the detail status bar view at first. Fix #348. --- Evergreen/Base.lproj/MainWindow.storyboard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index 588bc5c37..ea7e91e46 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -649,7 +649,7 @@ - + Detail diff --git a/Evergreen/MainWindow/Sidebar/SmartFeedPasteboardWriter.swift b/Evergreen/SmartFeeds/SmartFeedPasteboardWriter.swift similarity index 100% rename from Evergreen/MainWindow/Sidebar/SmartFeedPasteboardWriter.swift rename to Evergreen/SmartFeeds/SmartFeedPasteboardWriter.swift From 203637b30e86c79bcb93cff5d1bbaef007f252df Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 17 Feb 2018 22:23:36 -0800 Subject: [PATCH 67/84] Show a star in the timeline for starred articles. --- Evergreen/AppImages.swift | 5 +++ .../timelineStar.imageset/Contents.json | 22 ++++++++++++ .../timelineStar.imageset/timelineStar.png | Bin 0 -> 981 bytes .../timelineStar.imageset/timelineStar@2x.png | Bin 0 -> 1303 bytes .../Timeline/Cell/TimelineCellData.swift | 3 ++ .../Timeline/Cell/TimelineCellLayout.swift | 2 +- .../Timeline/Cell/TimelineTableCellView.swift | 33 ++++++++++++------ Evergreen/Resources/DB5.plist | 2 +- 8 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 Evergreen/Assets.xcassets/timelineStar.imageset/Contents.json create mode 100644 Evergreen/Assets.xcassets/timelineStar.imageset/timelineStar.png create mode 100644 Evergreen/Assets.xcassets/timelineStar.imageset/timelineStar@2x.png diff --git a/Evergreen/AppImages.swift b/Evergreen/AppImages.swift index 6b08ab7b7..17c160577 100644 --- a/Evergreen/AppImages.swift +++ b/Evergreen/AppImages.swift @@ -11,6 +11,7 @@ import AppKit extension NSImage.Name { static let star = NSImage.Name(rawValue: "star") static let unstar = NSImage.Name(rawValue: "unstar") + static let timelineStar = NSImage.Name(rawValue: "timelineStar") } struct AppImages { @@ -20,4 +21,8 @@ struct AppImages { let image = NSImage(contentsOfFile: path) return image }() + + static var timelineStar: NSImage! = { + return NSImage(named: .timelineStar) + }() } diff --git a/Evergreen/Assets.xcassets/timelineStar.imageset/Contents.json b/Evergreen/Assets.xcassets/timelineStar.imageset/Contents.json new file mode 100644 index 000000000..838695523 --- /dev/null +++ b/Evergreen/Assets.xcassets/timelineStar.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "timelineStar.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "timelineStar@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Evergreen/Assets.xcassets/timelineStar.imageset/timelineStar.png b/Evergreen/Assets.xcassets/timelineStar.imageset/timelineStar.png new file mode 100644 index 0000000000000000000000000000000000000000..eb88ecbc7a1d5923b93f1bc871622a32339a7993 GIT binary patch literal 981 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4uAB#T}@sR2?f>5jgR3=A9lx&I`x0{IHb9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!CBxDSxV%QuQiw3xKK_7;Gx6fXv*~l0=1y+?>2(s|s5sunH?6 z8zii+qySb@l5MLL;TxdfoL`ixV5(=LXP{)qrJ$f-QN^PGMBb474qg}`6R4!DO34IM^VA{b?=*2cj{z3lsT-EfA6-rHT!?jI+w%z zl^>W_ZM60_oG^#){guciqOXs;_L=#W=ohNIQGb>nx%9|i^?9e(rg7?Twz+(VnNfQ4 z1O4ZhllH8;SFrpEf2FHiz16(^OMF_Hdz?MB1(%zD{gyNP@gno@YV`*vPWaogB0blS z$!@=`yHd5%Ocuw${d4|En*N>bu9&*kDtK>TeQ^5a?>?tCeO-1vbEl$_;2ai9;nXDS zoR!;JY**@SJW#cuu3Lsz@Lc86%-CbB2g{yL+obj1DH3!`38u<7Y53BN&^$J5o% JWt~$(695U_N@oB7 literal 0 HcmV?d00001 diff --git a/Evergreen/Assets.xcassets/timelineStar.imageset/timelineStar@2x.png b/Evergreen/Assets.xcassets/timelineStar.imageset/timelineStar@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4bd2acdf11b004b9e6245d84509755b8ae1003cd GIT binary patch literal 1303 zcmeAS@N?(olHy`uVBq!ia0vp^QXtI11|(N{`J4k%Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8X6a5n0T@Af^h!jGjR% z9~c;zj50$aN+NuHtdjF{^%7I^lT!66atnZ}85nFTtboki)RIJnirk#MVyg;UC9n!B zAR8pCucQE0Qj%?}6yY17;GAESs$i;TqGzCF$EBd4U{jQmW)z9|8>y;bpKhp88yV>WRp=I1=9MH?=;jqGLkxkLnOgw2D6bgmE1>`MD-sLz4fPE4v1uyFOhY&iMHfg0q7CdT zh-Egwps{i;N=+=uFAB-e&#`k%&M(SSC`&CW2D#8g&s5LCMju@f!m&0WQ>~oya|?=6 zi$PlKOl|Zr#L(482Bj9~=ahm1!Oqac1gZmB6kSIIRvpNqP#re z#|88aEIrt9ajde}1g0K;PZ!4!58l0D=e(=8=7qMuCR%X|j&?BqP z%Iw*<;OfhAizBSMLdzcTY&g^^cGdMy*&9yNoJCS?mYxS^x-HZG@T*NI)Rt}O>K$CI z&%Zpi{kPZP>V>9_GP_@?T=>s4(e1O%0>95aU$XBkE1G`naJ6AmX!2C&JAus=GV;s^ zcLg2~W}CZ_sa*M-a7qksqGQ*Q;|^EWUt#AkbH0}HW`b(DTb8MVhVW(QU!wdh8p7{p z9G{yP6p?n$*R-ok`U&eKg}+SdH;+YkEx)rwq_$CE`W1zEO{|~boyDYo*CP5le_`*%evYasWk21l7OHJbIj*99>a#aP#rY5BZ?@E13uvY# my?T-EclAl$6@lyjE?eDcc;;o=bm|kR`0;f0b6Mw<&;$T=^X)PK literal 0 HcmV?d00001 diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift index 314a52d4f..f7e6ff5e2 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift @@ -27,6 +27,7 @@ struct TimelineCellData { let showAvatar: Bool // Make space even when avatar is nil let featuredImage: NSImage? // image from within the article let read: Bool + let starred: Bool init(article: Article, appearance: TimelineCellAppearance, showFeedName: Bool, feedName: String?, avatar: NSImage?, showAvatar: Bool, featuredImage: NSImage?) { @@ -72,6 +73,7 @@ struct TimelineCellData { self.featuredImage = featuredImage self.read = article.status.read + self.starred = article.status.starred } init() { //Empty @@ -88,6 +90,7 @@ struct TimelineCellData { self.avatar = nil self.featuredImage = nil self.read = true + self.starred = false } static func emptyCache() { diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift index 9f08dba9d..daed6895b 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift @@ -126,7 +126,7 @@ private func rectForStar(_ cellData: TimelineCellData, _ appearance: TimelineCel r.size.width = appearance.starDimension r.size.height = appearance.starDimension r.origin.x = floor(unreadIndicatorRect.origin.x - ((appearance.starDimension - appearance.unreadCircleDimension) / 2.0)) - r.origin.y = unreadIndicatorRect.origin.y + r.origin.y = unreadIndicatorRect.origin.y - 3.0 return r } diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift index 23500c854..f200b1cb3 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -8,6 +8,7 @@ import Foundation import RSTextDrawing +import DB5 class TimelineTableCellView: NSTableCellView { @@ -25,13 +26,14 @@ class TimelineTableCellView: NSTableCellView { return imageView }() -// let faviconImageView: NSImageView = { -// let imageView = NSImageView(frame: NSRect(x: 0, y: 0, width: 16, height: 16)) -// imageView.imageScaling = .scaleProportionallyDown -// imageView.animates = false -// imageView.imageAlignment = .alignCenter -// return imageView -// }() + let starView: NSImageView = { + let imageView = NSImageView(frame: NSRect.zero) + imageView.imageScaling = .scaleNone + imageView.animates = false + imageView.imageAlignment = .alignCenter + imageView.image = AppImages.timelineStar + return imageView + }() var cellAppearance: TimelineCellAppearance! { didSet { @@ -91,7 +93,7 @@ class TimelineTableCellView: NSTableCellView { addSubviewAtInit(dateView, hidden: false) addSubviewAtInit(feedNameView, hidden: true) addSubviewAtInit(avatarImageView, hidden: false) -// addSubviewAtInit(faviconImageView, hidden: true) + addSubviewAtInit(starView, hidden: false) } override init(frame frameRect: NSRect) { @@ -140,6 +142,7 @@ class TimelineTableCellView: NSTableCellView { dateView.rs_setFrameIfNotEqual(layoutRects.dateRect) feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect) avatarImageView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect) + starView.rs_setFrameIfNotEqual(layoutRects.starRect) // faviconImageView.rs_setFrameIfNotEqual(layoutRects.faviconRect) } @@ -186,12 +189,18 @@ class TimelineTableCellView: NSTableCellView { } private func updateUnreadIndicator() { - - if unreadIndicatorView.isHidden != cellData.read { - unreadIndicatorView.isHidden = cellData.read + + let shouldHide = cellData.read || cellData.starred + if unreadIndicatorView.isHidden != shouldHide { + unreadIndicatorView.isHidden = shouldHide } } + private func updateStarView() { + + starView.isHidden = !cellData.starred + } + private func updateAvatar() { if !cellData.showAvatar { @@ -240,6 +249,7 @@ class TimelineTableCellView: NSTableCellView { updateDateView() updateFeedNameView() updateUnreadIndicator() + updateStarView() updateAvatar() // updateFavicon() } @@ -256,3 +266,4 @@ class TimelineTableCellView: NSTableCellView { } } } + diff --git a/Evergreen/Resources/DB5.plist b/Evergreen/Resources/DB5.plist index 196dc661c..6188f6b17 100644 --- a/Evergreen/Resources/DB5.plist +++ b/Evergreen/Resources/DB5.plist @@ -114,7 +114,7 @@ avatarCornerRadius 7 starDimension - 19 + 13 Detail From ad600884fc9daeeb22c3d465e14a7e86af5092c0 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 17 Feb 2018 22:29:40 -0800 Subject: [PATCH 68/84] Skip drawing a light gray background for unloaded (or nonexistent) avatars in the timeline. --- .../MainWindow/Timeline/Cell/TimelineTableCellView.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift index f200b1cb3..82ace3436 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -222,13 +222,6 @@ class TimelineTableCellView: NSTableCellView { avatarImageView.wantsLayer = true avatarImageView.layer?.cornerRadius = cellAppearance.avatarCornerRadius - if avatarImageView.image == nil { - avatarImageView.layer?.backgroundColor = NSColor(calibratedWhite: 0.0, alpha: 0.05).cgColor - } - else { - avatarImageView.layer?.backgroundColor = NSColor.clear.cgColor - } - } private func updateFavicon() { From 3894a9ea146907ae8dc839e57235f10325eafa13 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 17 Feb 2018 22:37:33 -0800 Subject: [PATCH 69/84] Add a private extension to TimelineTableCellView. --- .../Timeline/Cell/TimelineTableCellView.swift | 117 ++++++++---------- 1 file changed, 53 insertions(+), 64 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift index 82ace3436..b59e1b52d 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -8,16 +8,15 @@ import Foundation import RSTextDrawing -import DB5 class TimelineTableCellView: NSTableCellView { - let titleView = RSMultiLineView(frame: NSZeroRect) - let unreadIndicatorView = UnreadIndicatorView(frame: NSZeroRect) - let dateView = RSSingleLineView(frame: NSZeroRect) - let feedNameView = RSSingleLineView(frame: NSZeroRect) + private let titleView = RSMultiLineView(frame: NSZeroRect) + private let unreadIndicatorView = UnreadIndicatorView(frame: NSZeroRect) + private let dateView = RSSingleLineView(frame: NSZeroRect) + private let feedNameView = RSSingleLineView(frame: NSZeroRect) - let avatarImageView: NSImageView = { + private let avatarImageView: NSImageView = { let imageView = NSImageView(frame: NSRect.zero) imageView.imageScaling = .scaleProportionallyDown imageView.animates = false @@ -26,7 +25,7 @@ class TimelineTableCellView: NSTableCellView { return imageView }() - let starView: NSImageView = { + private let starView: NSImageView = { let imageView = NSImageView(frame: NSRect.zero) imageView.imageScaling = .scaleNone imageView.animates = false @@ -79,23 +78,6 @@ class TimelineTableCellView: NSTableCellView { } } - private func addSubviewAtInit(_ view: NSView, hidden: Bool) { - - addSubview(view) - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = hidden - } - - private func commonInit() { - - addSubviewAtInit(titleView, hidden: false) - addSubviewAtInit(unreadIndicatorView, hidden: true) - addSubviewAtInit(dateView, hidden: false) - addSubviewAtInit(feedNameView, hidden: true) - addSubviewAtInit(avatarImageView, hidden: false) - addSubviewAtInit(starView, hidden: false) - } - override init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -124,11 +106,6 @@ class TimelineTableCellView: NSTableCellView { updateAppearance() } - private func updatedLayoutRects() -> TimelineCellLayout { - - return timelineCellLayout(NSWidth(bounds), cellData: cellData, appearance: cellAppearance) - } - override func layout() { resizeSubviews(withOldSize: NSZeroSize) @@ -143,7 +120,6 @@ class TimelineTableCellView: NSTableCellView { feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect) avatarImageView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect) starView.rs_setFrameIfNotEqual(layoutRects.starRect) -// faviconImageView.rs_setFrameIfNotEqual(layoutRects.faviconRect) } override func updateLayer() { @@ -160,20 +136,59 @@ class TimelineTableCellView: NSTableCellView { layer?.backgroundColor = color.cgColor } } +} - private func updateTitleView() { +// MARK: - Private + +private extension TimelineTableCellView { + + func addSubviewAtInit(_ view: NSView, hidden: Bool) { + + addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = hidden + } + + func commonInit() { + + addSubviewAtInit(titleView, hidden: false) + addSubviewAtInit(unreadIndicatorView, hidden: true) + addSubviewAtInit(dateView, hidden: false) + addSubviewAtInit(feedNameView, hidden: true) + addSubviewAtInit(avatarImageView, hidden: false) + addSubviewAtInit(starView, hidden: false) + } + + func updatedLayoutRects() -> TimelineCellLayout { + + return timelineCellLayout(NSWidth(bounds), cellData: cellData, appearance: cellAppearance) + } + + func updateAppearance() { + + if let rowView = superview as? NSTableRowView { + isEmphasized = rowView.isEmphasized + isSelected = rowView.isSelected + } + else { + isEmphasized = false + isSelected = false + } + } + + func updateTitleView() { titleView.attributedStringValue = cellData.attributedTitle needsLayout = true } - - private func updateDateView() { + + func updateDateView() { dateView.attributedStringValue = cellData.attributedDateString needsLayout = true } - private func updateFeedNameView() { + func updateFeedNameView() { if cellData.showFeedName { if feedNameView.isHidden { @@ -188,7 +203,7 @@ class TimelineTableCellView: NSTableCellView { } } - private func updateUnreadIndicator() { + func updateUnreadIndicator() { let shouldHide = cellData.read || cellData.starred if unreadIndicatorView.isHidden != shouldHide { @@ -196,12 +211,12 @@ class TimelineTableCellView: NSTableCellView { } } - private func updateStarView() { + func updateStarView() { starView.isHidden = !cellData.starred } - private func updateAvatar() { + func updateAvatar() { if !cellData.showAvatar { avatarImageView.image = nil @@ -224,19 +239,7 @@ class TimelineTableCellView: NSTableCellView { avatarImageView.layer?.cornerRadius = cellAppearance.avatarCornerRadius } - private func updateFavicon() { - -// if let favicon = cellData.showFeedName ? cellData.favicon : nil { -// faviconImageView.image = favicon -// faviconImageView.isHidden = false -// } -// else { -// faviconImageView.image = nil -// faviconImageView.isHidden = true -// } - } - - private func updateSubviews() { + func updateSubviews() { updateTitleView() updateDateView() @@ -244,19 +247,5 @@ class TimelineTableCellView: NSTableCellView { updateUnreadIndicator() updateStarView() updateAvatar() -// updateFavicon() - } - - private func updateAppearance() { - - if let rowView = superview as? NSTableRowView { - isEmphasized = rowView.isEmphasized - isSelected = rowView.isSelected - } - else { - isEmphasized = false - isSelected = false - } } } - From 3731648d57a74e1d610c9b79cd37ac82a44d74ec Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 12:09:13 -0800 Subject: [PATCH 70/84] Mark articles starred/unstarred via contextual menu in the timeline. --- ...melineViewController+ContextualMenus.swift | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift index 44aff4df0..c1a99b904 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -49,10 +49,18 @@ extension TimelineViewController { @objc func markArticlesStarredFromContextualMenu(_ sender: Any?) { + guard let articles = articles(from: sender) else { + return + } + markArticles(articles, starred: true) } @objc func markArticlesUnstarredFromContextualMenu(_ sender: Any?) { + guard let articles = articles(from: sender) else { + return + } + markArticles(articles, starred: false) } @objc func openInBrowserFromContextualMenu(_ sender: Any?) { @@ -69,14 +77,21 @@ private extension TimelineViewController { func markArticles(_ articles: [Article], read: Bool) { - guard let articlesToMark = read ? unreadArticles(from: articles) : readArticles(from: articles) else { - return - } - guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articlesToMark), markingRead: read, undoManager: undoManager) else { + markArticles(articles, statusKey: .read, flag: read) + } + + func markArticles(_ articles: [Article], starred: Bool) { + + markArticles(articles, statusKey: .starred, flag: starred) + } + + func markArticles(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool) { + + guard let undoManager = undoManager, let markStatusCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager) else { return } - runCommand(markReadCommand) + runCommand(markStatusCommand) } func unreadArticles(from articles: [Article]) -> [Article]? { @@ -110,12 +125,12 @@ private extension TimelineViewController { menu.addItem(NSMenuItem.separator()) } -// if articles.anyArticleIsUnstarred() { -// menu.addItem(markStarredMenuItem(articles)) -// } -// if articles.anyArticleIsStarred() { -// menu.addItem(markUnstarredMenuItem(articles)) -// } + if articles.anyArticleIsUnstarred() { + menu.addItem(markStarredMenuItem(articles)) + } + if articles.anyArticleIsStarred() { + menu.addItem(markUnstarredMenuItem(articles)) + } if menu.items.count > 0 && !menu.items.last!.isSeparatorItem { menu.addItem(NSMenuItem.separator()) } From 24db26777f9b4b11018ecb0e821e97de5fcee761 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 12:45:18 -0800 Subject: [PATCH 71/84] Release 1.0d38. --- Appcasts/evergreen-beta.xml | 18 ++++++++++++++++++ Evergreen/Info.plist | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index 139fc1b21..7021be465 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -6,6 +6,24 @@ Most recent Evergreen changes with links to updates. en + + Evergreen 1.0d38 + Starring +

You can mark articles starred and unstarred — via the menu, contextual menu in the timeline, and toolbar button.

+

The Starred smart feed shows all your starred articles. Tip: cmd-3 takes you to the Starred feed.

+ +

Misc.

+

Added special cases to the JSON Feed parser for known feeds that put HTML entities in article titles. (Kottke, Pixel Envy.)

+

Revised the JSON parser feed detector to allow for a version property that uses the incorrect scheme. (Makes Pixel Envy’s feed work.)

+

Made Kottke’s feed a default for new users.

+ ]]>
+ Sun, 18 Feb 2018 12:30:00 -0800 + + 10.13 +
+ Evergreen 1.0d37 CFBundlePackageType APPL CFBundleShortVersionString - 1.0d37 + 1.0d38 CFBundleVersion 522 LSMinimumSystemVersion From 994426ffa1a02185ae6d09f4a9f70c523b5898a7 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 13:00:54 -0800 Subject: [PATCH 72/84] Make drawing the grid in the timeline a DB5 option. --- .../MainWindow/Timeline/Cell/TimelineCellAppearance.swift | 4 +++- Evergreen/MainWindow/Timeline/TimelineTableRowView.swift | 8 ++++---- Evergreen/Resources/DB5.plist | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift index 2d4fc481a..4c7218f0f 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift @@ -34,6 +34,7 @@ struct TimelineCellAppearance: Equatable { let starDimension: CGFloat let gridColor: NSColor + let drawsGrid: Bool let avatarSize: NSSize let avatarMarginRight: CGFloat @@ -71,7 +72,8 @@ struct TimelineCellAppearance: Equatable { self.starDimension = theme.float(forKey: "MainWindow.Timeline.cell.starDimension") self.gridColor = theme.colorWithAlpha(forKey: "MainWindow.Timeline.gridColor") - + self.drawsGrid = theme.bool(forKey: "MainWindow.Timeline.drawsGrid") + self.avatarSize = theme.size(forKey: "MainWindow.Timeline.cell.avatar") self.avatarMarginRight = theme.float(forKey: "MainWindow.Timeline.cell.avatarMarginRight") self.avatarAdjustmentTop = theme.float(forKey: "MainWindow.Timeline.cell.avatarAdjustmentTop") diff --git a/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift b/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift index 312fc92ee..03ed5b125 100644 --- a/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift +++ b/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift @@ -17,11 +17,11 @@ class TimelineTableRowView : NSTableRowView { } } } - + // override var interiorBackgroundStyle: NSBackgroundStyle { // return .Light // } - + private var cellView: TimelineTableCellView? { for oneSubview in subviews { if let foundView = oneSubview as? TimelineTableCellView { @@ -50,7 +50,7 @@ class TimelineTableRowView : NSTableRowView { var gridRect: NSRect { return NSMakeRect(0.0, NSMaxY(bounds) - 1.0, NSWidth(bounds), 1) } - + override func drawSeparator(in dirtyRect: NSRect) { let path = NSBezierPath() @@ -68,7 +68,7 @@ class TimelineTableRowView : NSTableRowView { super.draw(dirtyRect) - if !isSelected && !isNextRowSelected { + if cellAppearance.drawsGrid && !isSelected && !isNextRowSelected { drawSeparator(in: dirtyRect) } } diff --git a/Evergreen/Resources/DB5.plist b/Evergreen/Resources/DB5.plist index 6188f6b17..7d1dc6ca1 100644 --- a/Evergreen/Resources/DB5.plist +++ b/Evergreen/Resources/DB5.plist @@ -66,6 +66,8 @@ 000000 gridColorAlpha 0.1 + drawsGrid + header backgroundColor From 1e250839c335ec4fc5ca21debaa7495077f3caf0 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 13:01:34 -0800 Subject: [PATCH 73/84] Remove some commented-out code. --- .../Timeline/Cell/TimelineCellLayout.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift index daed6895b..ea3bf1fb2 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift @@ -75,21 +75,6 @@ private func rectForFeedName(_ cellData: TimelineCellData, _ width: CGFloat, _ a return r } -//private func rectForFavicon(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ feedNameRect: NSRect, _ unreadIndicatorRect: NSRect) -> NSRect { -// -// guard let _ = cellData.favicon, cellData.showFeedName else { -// return NSZeroRect -// } -// -// var r = NSZeroRect -// r.size = appearance.faviconSize -// r.origin.y = feedNameRect.origin.y -// -// r = RSRectCenteredHorizontallyInRect(r, unreadIndicatorRect) -// -// return r -//} - private func rectsForTitle(_ cellData: TimelineCellData, _ width: CGFloat, _ appearance: TimelineCellAppearance) -> (NSRect, NSRect) { var r = NSZeroRect @@ -150,7 +135,6 @@ func timelineCellLayout(_ width: CGFloat, cellData: TimelineCellData, appearance let feedNameRect = rectForFeedName(cellData, width, appearance, dateRect) let unreadIndicatorRect = rectForUnreadIndicator(cellData, appearance, titleLine1Rect) let starRect = rectForStar(cellData, appearance, unreadIndicatorRect) -// let faviconRect = rectForFavicon(cellData, appearance, feedNameRect, unreadIndicatorRect) let avatarImageRect = rectForAvatar(cellData, appearance, titleLine1Rect) return TimelineCellLayout(width: width, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, paddingBottom: appearance.cellPadding.bottom) From 0ad41358fced6c84c470130a50cee2c0eaf54a57 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 15:13:47 -0800 Subject: [PATCH 74/84] Rewrite much of the timeline cell layout code. Move avatars to the right. --- .../Cell/TimelineCellAppearance.swift | 10 +- .../Timeline/Cell/TimelineCellLayout.swift | 195 ++++++++++-------- .../Timeline/Cell/TimelineTableCellView.swift | 2 +- .../Timeline/TimelineViewController.swift | 2 +- Evergreen/Resources/DB5.plist | 8 +- 5 files changed, 121 insertions(+), 96 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift index 4c7218f0f..71c8719d3 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift @@ -38,6 +38,7 @@ struct TimelineCellAppearance: Equatable { let avatarSize: NSSize let avatarMarginRight: CGFloat + let avatarMarginLeft: CGFloat let avatarAdjustmentTop: CGFloat let avatarCornerRadius: CGFloat let showAvatar: Bool @@ -76,14 +77,15 @@ struct TimelineCellAppearance: Equatable { self.avatarSize = theme.size(forKey: "MainWindow.Timeline.cell.avatar") self.avatarMarginRight = theme.float(forKey: "MainWindow.Timeline.cell.avatarMarginRight") + self.avatarMarginLeft = theme.float(forKey: "MainWindow.Timeline.cell.avatarMarginLeft") self.avatarAdjustmentTop = theme.float(forKey: "MainWindow.Timeline.cell.avatarAdjustmentTop") self.avatarCornerRadius = theme.float(forKey: "MainWindow.Timeline.cell.avatarCornerRadius") self.showAvatar = showAvatar - var margin = self.cellPadding.left + self.unreadCircleDimension + self.unreadCircleMarginRight - if showAvatar { - margin += (self.avatarSize.width + self.avatarMarginRight) - } + let margin = self.cellPadding.left + self.unreadCircleDimension + self.unreadCircleMarginRight +// if showAvatar { +// margin += (self.avatarSize.width + self.avatarMarginRight) +// } self.boxLeftMargin = margin } diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift index ea3bf1fb2..a53a5bfe2 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift @@ -32,116 +32,137 @@ struct TimelineCellLayout { self.starRect = starRect self.avatarImageRect = avatarImageRect self.paddingBottom = paddingBottom - - var height = max(0, feedNameRect.maxY) - height = max(height, dateRect.maxY) - height = max(height, titleRect.maxY) - height = max(height, unreadIndicatorRect.maxY) - height = max(height, avatarImageRect.maxY) - height += paddingBottom - self.height = height + + self.height = [feedNameRect, dateRect, titleRect, unreadIndicatorRect, avatarImageRect].maxY() + paddingBottom + } + + init(width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) { + + var textBoxRect = TimelineCellLayout.rectForTextBox(appearance, cellData, width) + + let (titleRect, titleLine1Rect) = TimelineCellLayout.rectsForTitle(textBoxRect, cellData) + let dateRect = TimelineCellLayout.rectForDate(textBoxRect, titleRect, appearance, cellData) + let feedNameRect = TimelineCellLayout.rectForFeedName(textBoxRect, dateRect, appearance, cellData) + let unreadIndicatorRect = TimelineCellLayout.rectForUnreadIndicator(appearance, titleLine1Rect) + let starRect = TimelineCellLayout.rectForStar(appearance, unreadIndicatorRect) + + textBoxRect.size.height = ceil([titleRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y) + let avatarImageRect = TimelineCellLayout.rectForAvatar(cellData, appearance, textBoxRect, width) + + let paddingBottom = appearance.cellPadding.bottom + + self.init(width: width, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, paddingBottom: paddingBottom) + } + + static func height(for width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) -> CGFloat { + + let layout = TimelineCellLayout(width: width, cellData: cellData, appearance: appearance) + return layout.height } } -private func rectForDate(_ cellData: TimelineCellData, _ width: CGFloat, _ appearance: TimelineCellAppearance, _ titleRect: NSRect) -> NSRect { - - let renderer = RSSingleLineRenderer(attributedTitle: cellData.attributedDateString) - var r = NSZeroRect - r.size = renderer.size +// MARK: - Calculate Rects - r.origin.y = NSMaxY(titleRect) + appearance.titleBottomMargin - r.origin.x = appearance.boxLeftMargin - - r.size.width = min(width - (r.origin.x + appearance.cellPadding.right), r.size.width) - r.size.width = max(r.size.width, 0.0) +private extension TimelineCellLayout { - return r -} + static func rectForTextBox(_ appearance: TimelineCellAppearance, _ cellData: TimelineCellData, _ width: CGFloat) -> NSRect { -private func rectForFeedName(_ cellData: TimelineCellData, _ width: CGFloat, _ appearance: TimelineCellAppearance, _ dateRect: NSRect) -> NSRect { - - if !cellData.showFeedName { - return NSZeroRect + // Returned height is a placeholder. Not needed when this is calculated. + + let textBoxOriginX = appearance.cellPadding.left + appearance.unreadCircleDimension + appearance.unreadCircleMarginRight + let textBoxMaxX = floor((width - appearance.cellPadding.right) - (cellData.showAvatar ? appearance.avatarSize.width + appearance.avatarMarginLeft : 0.0)) + let textBoxWidth = floor(textBoxMaxX - textBoxOriginX) + let textBoxRect = NSRect(x: textBoxOriginX, y: appearance.cellPadding.top, width: textBoxWidth, height: 1000000) + + return textBoxRect } - let renderer = RSSingleLineRenderer(attributedTitle: cellData.attributedFeedName) - var r = NSZeroRect - r.size = renderer.size - r.origin.y = NSMaxY(dateRect) + appearance.titleBottomMargin - r.origin.x = appearance.boxLeftMargin - - r.size.width = max(0, width - (r.origin.x + appearance.cellPadding.right)) - - return r -} + static func rectsForTitle(_ textBoxRect: NSRect, _ cellData: TimelineCellData) -> (NSRect, NSRect) { -private func rectsForTitle(_ cellData: TimelineCellData, _ width: CGFloat, _ appearance: TimelineCellAppearance) -> (NSRect, NSRect) { - - var r = NSZeroRect - r.origin.x = appearance.boxLeftMargin - r.origin.y = appearance.cellPadding.top + var r = textBoxRect + let renderer = RSMultiLineRenderer(attributedTitle: cellData.attributedTitle) - let textWidth = width - (r.origin.x + appearance.cellPadding.right) - let renderer = RSMultiLineRenderer(attributedTitle: cellData.attributedTitle) + let measurements = renderer.measurements(forWidth: textBoxRect.width) + r.size.height = CGFloat(measurements.height) - let measurements = renderer.measurements(forWidth: textWidth) - r.size = NSSize(width: textWidth, height: CGFloat(measurements.height)) - r.size.width = max(r.size.width, 0.0) + var rline1 = r + rline1.size.height = CGFloat(measurements.heightOfFirstLine) - var rline1 = r - rline1.size.height = CGFloat(measurements.heightOfFirstLine) - - return (r, rline1) -} + return (r, rline1) + } -private func rectForUnreadIndicator(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ titleLine1Rect: NSRect) -> NSRect { - - var r = NSZeroRect - r.size = NSSize(width: appearance.unreadCircleDimension, height: appearance.unreadCircleDimension) - r.origin.x = appearance.cellPadding.left - r = RSRectCenteredVerticallyInRect(r, titleLine1Rect) - r.origin.y += 1 - - return r -} + static func rectForDate(_ textBoxRect: NSRect, _ titleRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { -private func rectForStar(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ unreadIndicatorRect: NSRect) -> NSRect { + return rectOfLineBelow(textBoxRect, titleRect, appearance.titleBottomMargin, cellData.attributedDateString) + } - var r = NSRect.zero - r.size.width = appearance.starDimension - r.size.height = appearance.starDimension - r.origin.x = floor(unreadIndicatorRect.origin.x - ((appearance.starDimension - appearance.unreadCircleDimension) / 2.0)) - r.origin.y = unreadIndicatorRect.origin.y - 3.0 - return r -} + static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { -private func rectForAvatar(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ titleLine1Rect: NSRect) -> NSRect { + if !cellData.showFeedName { + return NSZeroRect + } + + return rectOfLineBelow(textBoxRect, dateRect, appearance.titleBottomMargin, cellData.attributedFeedName) + } + + static func rectOfLineBelow(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ topMargin: CGFloat, _ attributedString: NSAttributedString) -> NSRect { + + let renderer = RSSingleLineRenderer(attributedTitle: attributedString) + var r = NSZeroRect + r.size = renderer.size + r.origin.y = NSMaxY(rectAbove) + topMargin + r.origin.x = textBoxRect.origin.x + + var width = renderer.size.width + width = min(width, textBoxRect.size.width) + width = max(width, 0.0) + r.size.width = width - var r = NSRect.zero - if !cellData.showAvatar { return r } - r.size = appearance.avatarSize - r.origin.x = appearance.cellPadding.left + appearance.unreadCircleDimension + appearance.unreadCircleMarginRight - r.origin.y = titleLine1Rect.minY + appearance.avatarAdjustmentTop - return r + static func rectForUnreadIndicator(_ appearance: TimelineCellAppearance, _ titleLine1Rect: NSRect) -> NSRect { + + var r = NSZeroRect + r.size = NSSize(width: appearance.unreadCircleDimension, height: appearance.unreadCircleDimension) + r.origin.x = appearance.cellPadding.left + r = RSRectCenteredVerticallyInRect(r, titleLine1Rect) + r.origin.y += 1 + + return r + } + + static func rectForStar(_ appearance: TimelineCellAppearance, _ unreadIndicatorRect: NSRect) -> NSRect { + + var r = NSRect.zero + r.size.width = appearance.starDimension + r.size.height = appearance.starDimension + r.origin.x = floor(unreadIndicatorRect.origin.x - ((appearance.starDimension - appearance.unreadCircleDimension) / 2.0)) + r.origin.y = unreadIndicatorRect.origin.y - 3.0 + return r + } + + static func rectForAvatar(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ textBoxRect: NSRect, _ width: CGFloat) -> NSRect { + + var r = NSRect.zero + if !cellData.showAvatar { + return r + } + r.size = appearance.avatarSize + r.origin.x = (width - appearance.cellPadding.right) - r.size.width + r = RSRectCenteredVerticallyInRect(r, textBoxRect) + + return r + } } -func timelineCellLayout(_ width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) -> TimelineCellLayout { +private extension Array where Element == NSRect { - let (titleRect, titleLine1Rect) = rectsForTitle(cellData, width, appearance) - let dateRect = rectForDate(cellData, width, appearance, titleRect) - let feedNameRect = rectForFeedName(cellData, width, appearance, dateRect) - let unreadIndicatorRect = rectForUnreadIndicator(cellData, appearance, titleLine1Rect) - let starRect = rectForStar(cellData, appearance, unreadIndicatorRect) - let avatarImageRect = rectForAvatar(cellData, appearance, titleLine1Rect) + func maxY() -> CGFloat { - return TimelineCellLayout(width: width, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, paddingBottom: appearance.cellPadding.bottom) + var y: CGFloat = 0.0 + self.forEach { y = Swift.max(y, $0.maxY) } + return y + } } -func timelineCellHeight(_ width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) -> CGFloat { - - let layout = timelineCellLayout(width, cellData: cellData, appearance: appearance) - return layout.height -} diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift index b59e1b52d..a7971a06d 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -161,7 +161,7 @@ private extension TimelineTableCellView { func updatedLayoutRects() -> TimelineCellLayout { - return timelineCellLayout(NSWidth(bounds), cellData: cellData, appearance: cellAppearance) + return TimelineCellLayout(width: bounds.width, cellData: cellData, appearance: cellAppearance) } func updateAppearance() { diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 5d1d9a465..531f4f7ed 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -441,7 +441,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: nil, dateModified: nil, authors: nil, attachments: nil, status: status) let prototypeCellData = TimelineCellData(article: prototypeArticle, appearance: cellAppearance, showFeedName: showingFeedNames, feedName: "Prototype Feed Name", avatar: nil, showAvatar: false, featuredImage: nil) - let height = timelineCellHeight(100, cellData: prototypeCellData, appearance: cellAppearance) + let height = TimelineCellLayout.height(for: 100, cellData: prototypeCellData, appearance: cellAppearance) return height } diff --git a/Evergreen/Resources/DB5.plist b/Evergreen/Resources/DB5.plist index 7d1dc6ca1..227278ee5 100644 --- a/Evergreen/Resources/DB5.plist +++ b/Evergreen/Resources/DB5.plist @@ -76,13 +76,13 @@ cell paddingLeft - 12 + 20 paddingRight 12 paddingTop - 12 - paddingBottom 14 + paddingBottom + 16 feedNameColor 999999 faviconFeedNameSpacing @@ -110,6 +110,8 @@ avatarWidth 48 avatarMarginRight + 20 + avatarMarginLeft 8 avatarAdjustmentTop 4 From 3d5be1022c22341bd610e1a35d5b3bf770028b33 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 16:13:58 -0800 Subject: [PATCH 75/84] Define a textOnlyColor for when an article has no title. --- .../Timeline/Cell/TimelineCellAppearance.swift | 11 +++++++++-- .../MainWindow/Timeline/Cell/TimelineCellData.swift | 4 ++-- Evergreen/Resources/DB5.plist | 10 +++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift index 71c8719d3..048313692 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift @@ -26,7 +26,10 @@ struct TimelineCellAppearance: Equatable { let textColor: NSColor let textFont: NSFont - + + let textOnlyColor: NSColor + let textOnlyFont: NSFont + let unreadCircleColor: NSColor let unreadCircleDimension: CGFloat let unreadCircleMarginRight: CGFloat @@ -60,11 +63,15 @@ struct TimelineCellAppearance: Equatable { self.dateMarginLeft = theme.float(forKey: "MainWindow.Timeline.cell.dateMarginLeft") self.titleColor = theme.color(forKey: "MainWindow.Timeline.cell.titleColor") - self.titleFont = NSFont.systemFont(ofSize: actualFontSize, weight: NSFont.Weight.bold) + let titleFontSizeMultiplier = theme.float(forKey: "MainWindow.Timeline.cell.titleFontSizeMultiplier") + self.titleFont = NSFont.systemFont(ofSize: floor(actualFontSize * titleFontSizeMultiplier), weight: NSFont.Weight.semibold) self.titleBottomMargin = theme.float(forKey: "MainWindow.Timeline.cell.titleMarginBottom") self.textColor = theme.color(forKey: "MainWindow.Timeline.cell.textColor") self.textFont = NSFont.systemFont(ofSize: actualFontSize) + + self.textOnlyColor = theme.color(forKey: "MainWindow.Timeline.cell.textOnlyColor") + self.textOnlyFont = self.textFont self.unreadCircleColor = theme.color(forKey: "MainWindow.Timeline.cell.unreadCircleColor") self.unreadCircleDimension = theme.float(forKey: "MainWindow.Timeline.cell.unreadCircleDimension") diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift index f7e6ff5e2..95dd388e2 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift @@ -30,7 +30,7 @@ struct TimelineCellData { let starred: Bool init(article: Article, appearance: TimelineCellAppearance, showFeedName: Bool, feedName: String?, avatar: NSImage?, showAvatar: Bool, featuredImage: NSImage?) { - + self.title = timelineTruncatedTitle(article) self.text = timelineTruncatedSummary(article) @@ -117,6 +117,6 @@ private func attributedTitleString(_ title: String, _ text: String, _ appearance return NSAttributedString(string: title, attributes: [NSAttributedStringKey.foregroundColor: appearance.titleColor, NSAttributedStringKey.font: appearance.titleFont]) } - return NSAttributedString(string: text, attributes: [NSAttributedStringKey.foregroundColor: appearance.textColor, NSAttributedStringKey.font: appearance.textFont]) + return NSAttributedString(string: text, attributes: [NSAttributedStringKey.foregroundColor: appearance.textOnlyColor, NSAttributedStringKey.font: appearance.textFont]) } diff --git a/Evergreen/Resources/DB5.plist b/Evergreen/Resources/DB5.plist index 227278ee5..b61d63f4d 100644 --- a/Evergreen/Resources/DB5.plist +++ b/Evergreen/Resources/DB5.plist @@ -78,13 +78,13 @@ paddingLeft 20 paddingRight - 12 + 20 paddingTop 14 paddingBottom 16 feedNameColor - 999999 + aaaaaa faviconFeedNameSpacing 2 dateColor @@ -94,9 +94,13 @@ dateMarginBottom 2 textColor - 666666 + 999999 + textOnlyColor + 222222 titleColor 222222 + titleFontSizeMultiplier + 1.1 titleMarginBottom 1 unreadCircleColor From 2496f57af430ccad31265c65464dc62d8230f43d Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 16:57:09 -0800 Subject: [PATCH 76/84] Tweak some colors. --- Evergreen/Base.lproj/MainWindow.storyboard | 11 +++++------ Evergreen/Resources/DB5.plist | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index 3bffb2c55..777dffa61 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -533,18 +533,17 @@ - + - + - - + @@ -560,11 +559,11 @@ - + - + diff --git a/Evergreen/Resources/DB5.plist b/Evergreen/Resources/DB5.plist index b61d63f4d..a59a0bdff 100644 --- a/Evergreen/Resources/DB5.plist +++ b/Evergreen/Resources/DB5.plist @@ -80,7 +80,7 @@ paddingRight 20 paddingTop - 14 + 16 paddingBottom 16 feedNameColor @@ -94,7 +94,7 @@ dateMarginBottom 2 textColor - 999999 + aaaaaa textOnlyColor 222222 titleColor From 07aa49d51d6155314b57f5b1541a6c37aa562bf5 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 16:59:58 -0800 Subject: [PATCH 77/84] Bump version to 1.0d39. --- Evergreen/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evergreen/Info.plist b/Evergreen/Info.plist index c821e40ef..3a1282e8e 100644 --- a/Evergreen/Info.plist +++ b/Evergreen/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0d38 + 1.0d39 CFBundleVersion 522 LSMinimumSystemVersion From 1663fd614bbbe3ee972173f206d288c39c66980d Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 17:29:11 -0800 Subject: [PATCH 78/84] Update appcast. --- Appcasts/evergreen-beta.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index 7021be465..c26a2e4df 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -6,6 +6,22 @@ Most recent Evergreen changes with links to updates. en + + Evergreen 1.0d39 + Timeline +

Moved avatars and feed icons to the right.

+

Tweaked some colors and margins.

+

Removed the grid line.

+

Made the text for no-title articles darker.

+ + ]]>
+ Sun, 18 Feb 2018 17:00:00 -0800 + + 10.13 +
+ Evergreen 1.0d38 Date: Sun, 18 Feb 2018 20:13:47 -0800 Subject: [PATCH 79/84] Give the source list an almost-white background color, because favicons are created with the expectation of a white background, and they look way better this way than on a visual effects background. Plus, that blue source list thing has been so tired for so long. --- Evergreen/Base.lproj/MainWindow.storyboard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index 777dffa61..50cb3a871 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -277,7 +277,7 @@ - + From 04694cef4ce39c7e321ea3a5443e972f1de567ce Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 20:28:31 -0800 Subject: [PATCH 80/84] Tweak timeline font sizes a bit. --- .../Timeline/Cell/TimelineCellAppearance.swift | 18 +++++++++--------- .../Timeline/Cell/TimelineCellData.swift | 2 +- Evergreen/Resources/DB5.plist | 4 +--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift index 048313692..01e4d8bee 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift @@ -51,28 +51,28 @@ struct TimelineCellAppearance: Equatable { init(theme: VSTheme, showAvatar: Bool, fontSize: FontSize) { let actualFontSize = AppDefaults.actualFontSize(for: fontSize) - + let smallItemFontSize = floor(actualFontSize * 0.95) + let largeItemFontSize = floor(actualFontSize * 1.1) + self.cellPadding = theme.edgeInsets(forKey: "MainWindow.Timeline.cell.padding") self.feedNameColor = theme.color(forKey: "MainWindow.Timeline.cell.feedNameColor") - self.feedNameFont = NSFont.systemFont(ofSize: actualFontSize) + self.feedNameFont = NSFont.systemFont(ofSize: smallItemFontSize) self.dateColor = theme.color(forKey: "MainWindow.Timeline.cell.dateColor") - let actualDateFontSize = AppDefaults.actualFontSize(for: fontSize) - self.dateFont = NSFont.systemFont(ofSize: actualDateFontSize) + self.dateFont = NSFont.systemFont(ofSize: smallItemFontSize) self.dateMarginLeft = theme.float(forKey: "MainWindow.Timeline.cell.dateMarginLeft") self.titleColor = theme.color(forKey: "MainWindow.Timeline.cell.titleColor") - let titleFontSizeMultiplier = theme.float(forKey: "MainWindow.Timeline.cell.titleFontSizeMultiplier") - self.titleFont = NSFont.systemFont(ofSize: floor(actualFontSize * titleFontSizeMultiplier), weight: NSFont.Weight.semibold) + self.titleFont = NSFont.systemFont(ofSize: largeItemFontSize, weight: NSFont.Weight.semibold) self.titleBottomMargin = theme.float(forKey: "MainWindow.Timeline.cell.titleMarginBottom") self.textColor = theme.color(forKey: "MainWindow.Timeline.cell.textColor") - self.textFont = NSFont.systemFont(ofSize: actualFontSize) + self.textFont = NSFont.systemFont(ofSize: largeItemFontSize) self.textOnlyColor = theme.color(forKey: "MainWindow.Timeline.cell.textOnlyColor") - self.textOnlyFont = self.textFont - + self.textOnlyFont = NSFont.systemFont(ofSize: largeItemFontSize) + self.unreadCircleColor = theme.color(forKey: "MainWindow.Timeline.cell.unreadCircleColor") self.unreadCircleDimension = theme.float(forKey: "MainWindow.Timeline.cell.unreadCircleDimension") self.unreadCircleMarginRight = theme.float(forKey: "MainWindow.Timeline.cell.unreadCircleMarginRight") diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift index 95dd388e2..7e70e34ad 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift @@ -117,6 +117,6 @@ private func attributedTitleString(_ title: String, _ text: String, _ appearance return NSAttributedString(string: title, attributes: [NSAttributedStringKey.foregroundColor: appearance.titleColor, NSAttributedStringKey.font: appearance.titleFont]) } - return NSAttributedString(string: text, attributes: [NSAttributedStringKey.foregroundColor: appearance.textOnlyColor, NSAttributedStringKey.font: appearance.textFont]) + return NSAttributedString(string: text, attributes: [NSAttributedStringKey.foregroundColor: appearance.textOnlyColor, NSAttributedStringKey.font: appearance.textOnlyFont]) } diff --git a/Evergreen/Resources/DB5.plist b/Evergreen/Resources/DB5.plist index a59a0bdff..52de2341e 100644 --- a/Evergreen/Resources/DB5.plist +++ b/Evergreen/Resources/DB5.plist @@ -99,10 +99,8 @@ 222222 titleColor 222222 - titleFontSizeMultiplier - 1.1 titleMarginBottom - 1 + 2 unreadCircleColor #2db6ff unreadCircleDimension From 2bb3d5c6caf83d18beae59f2ea83d453be196e5c Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 20:37:51 -0800 Subject: [PATCH 81/84] Draw a light background for the no-content-view. --- Evergreen/Base.lproj/MainWindow.storyboard | 4 ++-- Evergreen/MainWindow/Detail/DetailViewController.swift | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index 50cb3a871..315e19190 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -269,7 +269,7 @@ - + @@ -535,7 +535,7 @@ - + diff --git a/Evergreen/MainWindow/Detail/DetailViewController.swift b/Evergreen/MainWindow/Detail/DetailViewController.swift index 64a6c82b9..94b331637 100644 --- a/Evergreen/MainWindow/Detail/DetailViewController.swift +++ b/Evergreen/MainWindow/Detail/DetailViewController.swift @@ -284,7 +284,8 @@ final class NoSelectionView: NSView { return } if let layer = layer { - let color = appDelegate.currentTheme.color(forKey: "MainWindow.Detail.noSelectionView.backgroundColor") +// let color = appDelegate.currentTheme.color(forKey: "MainWindow.Detail.noSelectionView.backgroundColor") + let color = NSColor(calibratedWhite: 0.96, alpha: 1.0) layer.backgroundColor = color.cgColor didConfigureLayer = true } From 1ba2306b9c8497e80c697b0b08fad1d4b9ecc04a Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 21:49:46 -0800 Subject: [PATCH 82/84] Show no-selection or multiple-selection text in the detail view when appropriate. --- Evergreen/Base.lproj/MainWindow.storyboard | 54 ++++++- .../Detail/DetailViewController.swift | 133 ++++++++++-------- .../MainWindow/Timeline/ArticleArray.swift | 6 + .../Timeline/TimelineViewController.swift | 24 ++-- 4 files changed, 147 insertions(+), 70 deletions(-) diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index 315e19190..ee93da7a0 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -2,7 +2,9 @@ + + @@ -699,11 +701,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Evergreen/MainWindow/Detail/DetailViewController.swift b/Evergreen/MainWindow/Detail/DetailViewController.swift index 94b331637..5e0e70e9e 100644 --- a/Evergreen/MainWindow/Detail/DetailViewController.swift +++ b/Evergreen/MainWindow/Detail/DetailViewController.swift @@ -12,14 +12,30 @@ import RSCore import Data import RSWeb -final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate { +final class DetailViewController: NSViewController, WKUIDelegate { @IBOutlet var containerView: DetailContainerView! + @IBOutlet var noSelectionView: NoSelectionView! var webview: DetailWebView! - var noSelectionView: NoSelectionView! - var article: Article? { + var articles: [Article]? { + didSet { + if let articles = articles, articles.count == 1 { + article = articles.first! + return + } + article = nil + if let _ = articles { + noSelectionView.showMultipleSelection() + } + else { + noSelectionView.showNoSelection() + } + } + } + + private var article: Article? { didSet { reloadHTML() showOrHideWebView() @@ -62,8 +78,6 @@ final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDe webview.customUserAgent = userAgent } - noSelectionView = NoSelectionView(frame: self.view.bounds) - containerView.viewController = self showOrHideWebView() @@ -91,7 +105,7 @@ final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDe webview.scrollPageDown(sender) } - // MARK: Notifications + // MARK: - Notifications @objc func timelineSelectionDidChange(_ notification: Notification) { @@ -102,8 +116,8 @@ final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDe return } - let timelineArticle = userInfo[UserInfoKey.article] as? Article - article = timelineArticle + let timelineArticles = userInfo[UserInfoKey.articles] as? ArticleArray + articles = timelineArticles } func viewWillStartLiveResize() { @@ -115,56 +129,30 @@ final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDe webview.evaluateJavaScript("document.body.style.overflow = 'visible';", completionHandler: nil) } - - // MARK: Private +} - private func reloadHTML() { +// MARK: - WKNavigationDelegate - if let article = article { - let articleRenderer = ArticleRenderer(article: article, style: ArticleStylesManager.shared.currentStyle) - webview.loadHTMLString(articleRenderer.html, baseURL: articleRenderer.baseURL) - } - else { - webview.loadHTMLString("", baseURL: nil) - } - } +extension DetailViewController: WKNavigationDelegate { - private func showOrHideWebView() { - - if let _ = article { - switchToView(webview) - } - else { - switchToView(noSelectionView) - } - } - - private func switchToView(_ view: NSView) { - - if containerView.contentView == view { - return - } - containerView.contentView = view - } - - // MARK: WKNavigationDelegate - public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - + if navigationAction.navigationType == .linkActivated { - + if let url = navigationAction.request.url { Browser.open(url.absoluteString) } - + decisionHandler(.cancel) return } - + decisionHandler(.allow) } } +// MARK: - WKScriptMessageHandler + extension DetailViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { @@ -200,8 +188,39 @@ extension DetailViewController: WKScriptMessageHandler { } } +// MARK: - Private + private extension DetailViewController { + func reloadHTML() { + + if let article = article { + let articleRenderer = ArticleRenderer(article: article, style: ArticleStylesManager.shared.currentStyle) + webview.loadHTMLString(articleRenderer.html, baseURL: articleRenderer.baseURL) + } + else { + webview.loadHTMLString("", baseURL: nil) + } + } + + func showOrHideWebView() { + + if let _ = article { + switchToView(webview) + } + else { + switchToView(noSelectionView) + } + } + + func switchToView(_ view: NSView) { + + if containerView.contentView == view { + return + } + containerView.contentView = view + } + func fetchScrollInfo(_ callback: @escaping (ScrollInfo?) -> Void) { let javascriptString = "var x = {contentHeight: document.body.scrollHeight, offsetY: document.body.scrollTop}; x" @@ -222,6 +241,8 @@ private extension DetailViewController { } } +// MARK: - + final class DetailContainerView: NSView { @IBOutlet var detailStatusBarView: DetailStatusBarView! @@ -270,28 +291,28 @@ final class DetailContainerView: NSView { } } +// MARK: - + final class NoSelectionView: NSView { - private var didConfigureLayer = false + @IBOutlet var noSelectionLabel: NSTextField! + @IBOutlet var multipleSelectionLabel: NSTextField! - override var wantsUpdateLayer: Bool { - return true + func showMultipleSelection() { + + noSelectionLabel.isHidden = true + multipleSelectionLabel.isHidden = false } - override func updateLayer() { + func showNoSelection() { - guard !didConfigureLayer else { - return - } - if let layer = layer { -// let color = appDelegate.currentTheme.color(forKey: "MainWindow.Detail.noSelectionView.backgroundColor") - let color = NSColor(calibratedWhite: 0.96, alpha: 1.0) - layer.backgroundColor = color.cgColor - didConfigureLayer = true - } + noSelectionLabel.isHidden = false + multipleSelectionLabel.isHidden = true } } +// MARK: - + private struct ScrollInfo { let contentHeight: CGFloat diff --git a/Evergreen/MainWindow/Timeline/ArticleArray.swift b/Evergreen/MainWindow/Timeline/ArticleArray.swift index b1a8d7512..67192fb94 100644 --- a/Evergreen/MainWindow/Timeline/ArticleArray.swift +++ b/Evergreen/MainWindow/Timeline/ArticleArray.swift @@ -110,6 +110,12 @@ extension Array where Element == Article { return anyArticlePassesTest { !$0.status.starred } } + + func unreadArticles() -> [Article]? { + + let articles = self.filter{ !$0.status.read } + return articles.isEmpty ? nil : articles + } } private extension Array where Element == Article { diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 531f4f7ed..2d1ea4635 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -516,30 +516,28 @@ extension TimelineViewController: NSTableViewDelegate { func tableViewSelectionDidChange(_ notification: Notification) { - tableView.redrawGrid() + // tableView.redrawGrid() - let selectedRow = tableView.selectedRow - if selectedRow < 0 || selectedRow == NSNotFound || tableView.numberOfSelectedRows != 1 { + if selectedArticles.isEmpty { postTimelineSelectionDidChangeNotification(nil) return } - if let selectedArticle = articles.articleAtRow(selectedRow) { - if (!selectedArticle.status.read) { - markArticles(Set([selectedArticle]), statusKey: .read, flag: true) + if selectedArticles.count == 1 { + let article = selectedArticles.first! + if !article.status.read { + markArticles(Set([article]), statusKey: .read, flag: true) } - postTimelineSelectionDidChangeNotification(selectedArticle) - } - else { - postTimelineSelectionDidChangeNotification(nil) } + + postTimelineSelectionDidChangeNotification(selectedArticles) } - private func postTimelineSelectionDidChangeNotification(_ selectedArticle: Article?) { + private func postTimelineSelectionDidChangeNotification(_ selectedArticles: ArticleArray?) { var userInfo = UserInfoDictionary() - if let article = selectedArticle { - userInfo[UserInfoKey.article] = article + if let selectedArticles = selectedArticles { + userInfo[UserInfoKey.articles] = selectedArticles } userInfo[UserInfoKey.view] = tableView From d8b437114486becdf3f5a5a247fc6386f469a58a Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 22:13:20 -0800 Subject: [PATCH 83/84] Make sidebar status view use same background color as source list. --- Evergreen/Base.lproj/MainWindow.storyboard | 2 +- Evergreen/Info.plist | 2 +- .../Sidebar/SidebarStatusBarView.swift | 19 ++++++++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index ee93da7a0..78bae30e7 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -361,7 +361,7 @@ - + diff --git a/Evergreen/Info.plist b/Evergreen/Info.plist index 3a1282e8e..511f4486c 100644 --- a/Evergreen/Info.plist +++ b/Evergreen/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0d39 + 1.0d40 CFBundleVersion 522 LSMinimumSystemVersion diff --git a/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift b/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift index d82c58e7b..dbd4df147 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift @@ -17,6 +17,8 @@ final class SidebarStatusBarView: NSView { @IBOutlet var progressIndicator: NSProgressIndicator! @IBOutlet var progressLabel: NSTextField! + private var didConfigureLayer = false + private var isAnimatingProgress = false { didSet { progressIndicator.isHidden = !isAnimatingProgress @@ -32,7 +34,11 @@ final class SidebarStatusBarView: NSView { override var isFlipped: Bool { return true } - + + override var wantsUpdateLayer: Bool { + return true + } + override func awakeFromNib() { progressIndicator.isHidden = true @@ -45,6 +51,17 @@ final class SidebarStatusBarView: NSView { NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) } + override func updateLayer() { + + guard let layer = layer, !didConfigureLayer else { + return + } + + let color = NSColor(calibratedWhite: 0.96, alpha: 1.0) + layer.backgroundColor = color.cgColor + didConfigureLayer = true + } + @objc func updateUI() { guard let progress = progress else { From 01b5292e3d4002a1d12d68d4b2f976cfc5ed7bd1 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 18 Feb 2018 22:49:58 -0800 Subject: [PATCH 84/84] Update appcast. --- Appcasts/evergreen-beta.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index c26a2e4df..6ef9c5233 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -6,6 +6,20 @@ Most recent Evergreen changes with links to updates. en + + Evergreen 1.0d40 + Improve font sizing in the timeline.

+

Show no-selection or multiple-selection text in the detail view when appropriate.

+

Give the source list an almost-white background (96%) instead of using the blueish blur view. Reason: favicons are often created with the expectation that they will be placed on a white background — they often don’t include transparency. They end up looking better on this almost-white background.

+ + ]]>
+ Sun, 18 Feb 2018 22:10:00 -0800 + + 10.13 +
+ Evergreen 1.0d39