diff --git a/Appcasts/evergreen-beta.xml b/Appcasts/evergreen-beta.xml index c216a76d6..6ef9c5233 100755 --- a/Appcasts/evergreen-beta.xml +++ b/Appcasts/evergreen-beta.xml @@ -6,6 +6,105 @@ 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 + 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 + 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 + 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 + 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 Bool)) -> MarkCommandValidationStatus { + + if articles.isEmpty { + return .canDoNothing + } + return canMarkTest(articles) ? .canMark : .canUnmark + } +} 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-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.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 5d46feaf2..8e85b9f37 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 8403E75B201C4A79007F7246 /* FeedListKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8403E75A201C4A79007F7246 /* FeedListKeyboardDelegate.swift */; }; 840D617F2029031C009BC708 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D617E2029031C009BC708 /* AppDelegate.swift */; }; 840D61812029031C009BC708 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61802029031C009BC708 /* MasterViewController.swift */; }; 840D61832029031C009BC708 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61822029031C009BC708 /* DetailViewController.swift */; }; @@ -17,6 +16,8 @@ 840D61962029031D009BC708 /* Evergreen_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61952029031D009BC708 /* Evergreen_iOSTests.swift */; }; 840D61A12029031E009BC708 /* Evergreen_iOSUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61A02029031E009BC708 /* Evergreen_iOSUITests.swift */; }; 8414AD251FCF5A1E00955102 /* TimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8414AD241FCF5A1E00955102 /* TimelineHeaderView.swift */; }; + 84162A152038C12C00035290 /* MarkCommandValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */; }; + 84162A252038C1E000035290 /* TimelineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84162A242038C1E000035290 /* TimelineDataSource.swift */; }; 841ABA4E20145E7300980E11 /* NothingInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */; }; 841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */; }; 841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */; }; @@ -29,6 +30,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 */; }; @@ -36,7 +38,6 @@ 844B5B651FEA11F200C7C76A /* GlobalKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B641FEA11F200C7C76A /* GlobalKeyboardShortcuts.plist */; }; 844B5B671FEA18E300C7C76A /* MainWIndowKeyboardHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B5B661FEA18E300C7C76A /* MainWIndowKeyboardHandler.swift */; }; 844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */; }; - 84513F901FAA63950023A1A9 /* FeedListControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */; }; 845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; }; 845479881FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; }; 845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; }; @@ -49,7 +50,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 */; }; @@ -98,12 +99,18 @@ 849C64681ED37A5D003D8FC0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 849C64671ED37A5D003D8FC0 /* Assets.xcassets */; }; 849C646B1ED37A5D003D8FC0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 849C64691ED37A5D003D8FC0 /* Main.storyboard */; }; 849C64761ED37A5D003D8FC0 /* EvergreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849C64751ED37A5D003D8FC0 /* EvergreenTests.swift */; }; + 849EE70F203919360082A1EA /* AppImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849EE70E203919360082A1EA /* AppImages.swift */; }; + 849EE71F20391DF20082A1EA /* MainWindowToolbarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849EE71E20391DF20082A1EA /* MainWindowToolbarDelegate.swift */; }; + 849EE72120391F560082A1EA /* MainWindowSharingServicePickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849EE72020391F560082A1EA /* MainWindowSharingServicePickerDelegate.swift */; }; 84A14FF320048CA70046AD9A /* SendToMicroBlogCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */; }; 84A1500320048D660046AD9A /* SendToCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500220048D660046AD9A /* SendToCommand.swift */; }; 84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; }; 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 */; }; + 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 */; }; @@ -128,7 +135,6 @@ 84BBB12D20142A4700F054F5 /* Inspector.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84BBB12B20142A4700F054F5 /* Inspector.storyboard */; }; 84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BBB12C20142A4700F054F5 /* InspectorWindowController.swift */; }; 84C12A151FF5B0080009A267 /* FeedList.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C12A141FF5B0080009A267 /* FeedList.storyboard */; }; - 84CC08061FF5D2E000C0C0ED /* FeedListSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC08051FF5D2E000C0C0ED /* FeedListSplitViewController.swift */; }; 84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; }; 84D52E951FE588BB00D14F5B /* DetailStatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */; }; 84D5BA20201E8FB6009092BD /* SidebarGearMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D5BA1F201E8FB6009092BD /* SidebarGearMenuDelegate.swift */; }; @@ -531,6 +537,8 @@ 840D61A02029031E009BC708 /* Evergreen_iOSUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Evergreen_iOSUITests.swift; sourceTree = ""; }; 840D61A22029031E009BC708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8414AD241FCF5A1E00955102 /* TimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderView.swift; sourceTree = ""; }; + 84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkCommandValidationStatus.swift; sourceTree = ""; }; + 84162A242038C1E000035290 /* TimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDataSource.swift; sourceTree = ""; }; 841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NothingInspectorViewController.swift; sourceTree = ""; }; 841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderInspectorViewController.swift; sourceTree = ""; }; 841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuiltinSmartFeedInspectorViewController.swift; sourceTree = ""; }; @@ -543,6 +551,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 = ""; }; @@ -562,7 +571,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 = ""; }; @@ -615,6 +624,9 @@ 849C64711ED37A5D003D8FC0 /* EvergreenTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EvergreenTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 849C64751ED37A5D003D8FC0 /* EvergreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvergreenTests.swift; sourceTree = ""; }; 849C64771ED37A5D003D8FC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 849EE70E203919360082A1EA /* AppImages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppImages.swift; path = Evergreen/AppImages.swift; sourceTree = ""; }; + 849EE71E20391DF20082A1EA /* MainWindowToolbarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindowToolbarDelegate.swift; sourceTree = ""; }; + 849EE72020391F560082A1EA /* MainWindowSharingServicePickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindowSharingServicePickerDelegate.swift; sourceTree = ""; }; 84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToMicroBlogCommand.swift; sourceTree = ""; }; 84A1500220048D660046AD9A /* SendToCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToCommand.swift; sourceTree = ""; }; 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToMarsEditCommand.swift; sourceTree = ""; }; @@ -623,6 +635,9 @@ 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 = ""; }; + 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 = ""; }; @@ -802,6 +817,8 @@ 849A97B01ED9FA69007D329B /* MainWindow.storyboard */, 842E45E21ED8C681000A8B52 /* KeyboardDelegateProtocol.swift */, 849A975D1ED9EB72007D329B /* MainWindowController.swift */, + 849EE71E20391DF20082A1EA /* MainWindowToolbarDelegate.swift */, + 849EE72020391F560082A1EA /* MainWindowSharingServicePickerDelegate.swift */, 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */, 844B5B6B1FEA224B00C7C76A /* Keyboard */, 849A975F1ED9EB95007D329B /* Sidebar */, @@ -893,7 +910,8 @@ 84702AB31FA27AE8006B8943 /* Commands */ = { isa = PBXGroup; children = ( - 84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */, + 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */, + 84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */, 84B99C9C1FAE83C600ECDEDB /* DeleteFromSidebarCommand.swift */, 84A1500220048D660046AD9A /* SendToCommand.swift */, 84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */, @@ -950,10 +968,12 @@ children = ( 849A97621ED9EB96007D329B /* SidebarViewController.swift */, 84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */, + 84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */, 849A97601ED9EB96007D329B /* SidebarOutlineView.swift */, 849A97611ED9EB96007D329B /* SidebarTreeControllerDelegate.swift */, 849A97631ED9EB96007D329B /* UnreadCountView.swift */, 845F52EC1FB2B9FC00C10BF0 /* FeedPasteboardWriter.swift */, + 84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */, 849A97821ED9EC63007D329B /* SidebarStatusBarView.swift */, 84D5BA1F201E8FB6009092BD /* SidebarGearMenuDelegate.swift */, 847FA120202BA34100BB56C8 /* SidebarContextualMenuDelegate.swift */, @@ -969,6 +989,7 @@ children = ( 849A976B1ED9EBC8007D329B /* TimelineViewController.swift */, 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */, + 84162A242038C1E000035290 /* TimelineDataSource.swift */, 84F204DF1FAACBB30076E152 /* ArticleArray.swift */, 849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */, 849A976A1ED9EBC8007D329B /* TimelineTableView.swift */, @@ -1022,14 +1043,13 @@ children = ( 84C12A141FF5B0080009A267 /* FeedList.storyboard */, 849A978C1ED9EE4D007D329B /* FeedListWindowController.swift */, - 84CC08051FF5D2E000C0C0ED /* FeedListSplitViewController.swift */, 84F204CD1FAACB660076E152 /* FeedListViewController.swift */, - 8403E75A201C4A79007F7246 /* FeedListKeyboardDelegate.swift */, - 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */, + 843A3B5520311E7700BF76EC /* FeedListOutlineView.swift */, 84B99C661FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift */, 84B99C681FAE36B800ECDEDB /* FeedListFolder.swift */, 84B99C6A1FAE370B00ECDEDB /* FeedListFeed.swift */, 84E95CF61FABB3C800552D99 /* FeedList.plist */, + 84DC413A20310AEE00198AD4 /* UnusedIn1.0 */, ); name = "Feed List"; path = Evergreen/FeedList; @@ -1085,6 +1105,7 @@ 849C64631ED37A5D003D8FC0 /* AppDelegate.swift */, 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */, 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */, + 849EE70E203919360082A1EA /* AppImages.swift */, 84DAEE311F870B390058304B /* DockBadge.swift */, 842E45DC1ED8C54B000A8B52 /* Browser.swift */, 84702AB31FA27AE8006B8943 /* Commands */, @@ -1267,6 +1288,16 @@ path = Importers; sourceTree = ""; }; + 84DC413A20310AEE00198AD4 /* UnusedIn1.0 */ = { + isa = PBXGroup; + children = ( + 84CC08051FF5D2E000C0C0ED /* FeedListSplitViewController.swift */, + 8403E75A201C4A79007F7246 /* FeedListKeyboardDelegate.swift */, + 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */, + ); + path = UnusedIn1.0; + sourceTree = ""; + }; 84EB380F1FBA8B9F000D2111 /* KeyboardShortcuts */ = { isa = PBXGroup; children = ( @@ -1285,6 +1316,7 @@ 845EE7C01FC2488C00854A1F /* SmartFeed.swift */, 84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */, 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */, + 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */, ); name = SmartFeeds; path = Evergreen/SmartFeeds; @@ -1870,7 +1902,6 @@ files = ( 84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */, 849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */, - 84513F901FAA63950023A1A9 /* FeedListControlsView.swift in Sources */, 84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */, 84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */, D5907D972004B7EB005947E5 /* Account+Scriptability.swift in Sources */, @@ -1888,20 +1919,21 @@ 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 */, D5A2678C20130ECF00A8D3C0 /* Author+Scriptability.swift in Sources */, 84F2D5371FC22FCC00998D64 /* PseudoFeed.swift in Sources */, 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 */, 849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */, 8426118A1FCB67AA0086A189 /* FeedIconDownloader.swift in Sources */, + 84162A152038C12C00035290 /* MarkCommandValidationStatus.swift in Sources */, 84E95D241FB1087500552D99 /* ArticlePasteboardWriter.swift in Sources */, 849A975B1ED9EB0D007D329B /* ArticleUtilities.swift in Sources */, 84DAEE301F86CAFE0058304B /* OPMLImporter.swift in Sources */, @@ -1917,12 +1949,17 @@ 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 */, + 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 */, + 849EE72120391F560082A1EA /* MainWindowSharingServicePickerDelegate.swift in Sources */, 849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */, + 849EE70F203919360082A1EA /* AppImages.swift in Sources */, 849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */, 84AAF2BF202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift in Sources */, 849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */, @@ -1945,12 +1982,12 @@ 842611A01FCB72600086A189 /* FeaturedImageDownloader.swift in Sources */, 849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */, 84E8E0EB202F693600562D8F /* DetailWebView.swift in Sources */, - 84CC08061FF5D2E000C0C0ED /* FeedListSplitViewController.swift in Sources */, 849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */, 849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */, 84B99C9D1FAE83C600ECDEDB /* DeleteFromSidebarCommand.swift in Sources */, 849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */, 849A976D1ED9EBC8007D329B /* TimelineTableView.swift in Sources */, + 84162A252038C1E000035290 /* TimelineDataSource.swift in Sources */, 84D52E951FE588BB00D14F5B /* DetailStatusBarView.swift in Sources */, D5E4CC64202C1AC1009B4FFC /* MainWindowController+Scriptability.swift in Sources */, 84B99C671FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift in Sources */, @@ -1965,6 +2002,7 @@ D5F4EDB720074D6500B9E363 /* Feed+Scriptability.swift in Sources */, 84E850861FCB60CE0072EA88 /* AuthorAvatarDownloader.swift in Sources */, 8414AD251FCF5A1E00955102 /* TimelineHeaderView.swift in Sources */, + 849EE71F20391DF20082A1EA /* MainWindowToolbarDelegate.swift in Sources */, 849A977A1ED9EC04007D329B /* TimelineTableCellView.swift in Sources */, 849A97761ED9EC04007D329B /* TimelineCellAppearance.swift in Sources */, 849A97A21ED9F180007D329B /* InitialFeedDownloader.swift in Sources */, diff --git a/Evergreen/AppDefaults.swift b/Evergreen/AppDefaults.swift index 46e17bc0d..6841b0b61 100644 --- a/Evergreen/AppDefaults.swift +++ b/Evergreen/AppDefaults.swift @@ -128,21 +128,24 @@ private extension AppDefaults { static func registerDefaults() { - let defaults = [Key.sidebarFontSize: FontSize.medium.rawValue, Key.timelineFontSize: FontSize.medium.rawValue, Key.detailFontSize: FontSize.medium.rawValue, Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue] + let defaults: [String : Any] = [Key.sidebarFontSize: FontSize.medium.rawValue, Key.timelineFontSize: FontSize.medium.rawValue, Key.detailFontSize: FontSize.medium.rawValue, Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, "NSScrollViewShouldScrollUnderTitlebar": false] UserDefaults.standard.register(defaults: defaults) } func fontSize(for key: String) -> FontSize { - var rawFontSize = int(for: key) - if rawFontSize < smallestFontSizeRawValue { - rawFontSize = smallestFontSizeRawValue - } - if rawFontSize > largestFontSizeRawValue { - rawFontSize = largestFontSizeRawValue - } - return FontSize(rawValue: rawFontSize)! + // Punted till after 1.0. + return .medium + +// var rawFontSize = int(for: key) +// if rawFontSize < smallestFontSizeRawValue { +// rawFontSize = smallestFontSizeRawValue +// } +// if rawFontSize > largestFontSizeRawValue { +// rawFontSize = largestFontSizeRawValue +// } +// return FontSize(rawValue: rawFontSize)! } func setFontSize(for key: String, _ fontSize: FontSize) { diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index d103ce06c..13e70cf76 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -26,17 +26,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, var authorAvatarDownloader: AuthorAvatarDownloader! var feedIconDownloader: FeedIconDownloader! var appName: String! - + @IBOutlet var debugMenuItem: NSMenuItem! @IBOutlet var sortByOldestArticleOnTopMenuItem: NSMenuItem! @IBOutlet var sortByNewestArticleOnTopMenuItem: NSMenuItem! - lazy var genericFeedImage: NSImage? = { - let path = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/BookmarkIcon.icns" - let image = NSImage(contentsOfFile: path) - return image - }() - lazy var sendToCommands: [SendToCommand] = { return [SendToMicroBlogCommand(), SendToMarsEditCommand()] }() @@ -273,7 +267,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 +276,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 +347,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 +372,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 +403,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } - @IBAction func addAppNews(_ sender: AnyObject) { + @IBAction func addAppNews(_ sender: Any?) { if AccountManager.shared.anyAccountHasFeedWithURL(appNewsURLString) { return @@ -417,17 +411,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,11 +431,16 @@ 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) } + @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() } @@ -494,9 +493,8 @@ private extension AppDelegate { func saveState() { - if let inspectorWindowController = inspectorWindowController { - inspectorWindowController.saveState() - } + inspectorWindowController?.saveState() + mainWindowController?.saveState() } func updateSortMenuItems() { diff --git a/Evergreen/AppImages.swift b/Evergreen/AppImages.swift new file mode 100644 index 000000000..17c160577 --- /dev/null +++ b/Evergreen/AppImages.swift @@ -0,0 +1,28 @@ +// +// AppImages.swift +// Evergreen +// +// Created by Brent Simmons on 2/17/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +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 { + + static var genericFeedImage: NSImage? = { + let path = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/BookmarkIcon.icns" + let image = NSImage(contentsOfFile: path) + return image + }() + + static var timelineStar: NSImage! = { + return NSImage(named: .timelineStar) + }() +} diff --git a/Evergreen/AppNotifications.swift b/Evergreen/AppNotifications.swift index b2e419cab..5ea7cac55 100644 --- a/Evergreen/AppNotifications.swift +++ b/Evergreen/AppNotifications.swift @@ -14,8 +14,6 @@ extension Notification.Name { static let SidebarSelectionDidChange = Notification.Name("SidebarSelectionDidChangeNotification") static let TimelineSelectionDidChange = Notification.Name("TimelineSelectionDidChangeNotification") - static let AppNavigationKeyPressed = Notification.Name("AppNavigationKeyPressedNotification") - static let UserDidAddFeed = Notification.Name("UserDidAddFeedNotification") // Sent by DetailViewController when mouse hovers over link in web view. 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 000000000..eb88ecbc7 Binary files /dev/null and b/Evergreen/Assets.xcassets/timelineStar.imageset/timelineStar.png differ 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 000000000..4bd2acdf1 Binary files /dev/null and b/Evergreen/Assets.xcassets/timelineStar.imageset/timelineStar@2x.png differ diff --git a/Evergreen/Base.lproj/Main.storyboard b/Evergreen/Base.lproj/Main.storyboard index 01e847f32..288fddd82 100644 --- a/Evergreen/Base.lproj/Main.storyboard +++ b/Evergreen/Base.lproj/Main.storyboard @@ -1,7 +1,7 @@ - + - + @@ -426,7 +426,11 @@ - + + + + + @@ -538,6 +542,13 @@ + + + + + + + diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index aa07aa7ad..78bae30e7 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -2,7 +2,9 @@ + + @@ -134,7 +136,7 @@ - + @@ -183,7 +185,7 @@ - + @@ -191,10 +193,12 @@ + + @@ -267,7 +271,7 @@ - + @@ -275,7 +279,7 @@ - + @@ -337,7 +341,6 @@ - @@ -358,7 +361,7 @@ - + @@ -532,18 +535,17 @@ - + - + - - + @@ -559,18 +561,18 @@ - + - + - + @@ -606,6 +608,7 @@ + @@ -636,6 +639,7 @@ + @@ -650,7 +654,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Evergreen/Base.lproj/Preferences.storyboard b/Evergreen/Base.lproj/Preferences.storyboard index 0ede97a0b..7db1ae995 100644 --- a/Evergreen/Base.lproj/Preferences.storyboard +++ b/Evergreen/Base.lproj/Preferences.storyboard @@ -1,7 +1,7 @@ - + - + @@ -31,11 +31,11 @@ - + - + @@ -44,7 +44,7 @@ - + @@ -64,7 +64,7 @@ - + @@ -73,7 +73,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 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/Data/SmallIconProvider.swift b/Evergreen/Data/SmallIconProvider.swift index b62fd2688..a602c359c 100644 --- a/Evergreen/Data/SmallIconProvider.swift +++ b/Evergreen/Data/SmallIconProvider.swift @@ -7,8 +7,27 @@ // import AppKit +import Data +import Account protocol SmallIconProvider { var smallIcon: NSImage? { get } } + +extension Feed: SmallIconProvider { + + var smallIcon: NSImage? { + if let image = appDelegate.faviconDownloader.favicon(for: self) { + return image + } + return AppImages.genericFeedImage + } +} + +extension Folder: SmallIconProvider { + + var smallIcon: NSImage? { + return NSImage(named: NSImage.Name.folder) + } +} diff --git a/Evergreen/DockBadge.swift b/Evergreen/DockBadge.swift index 3ac899833..c90ea6825 100644 --- a/Evergreen/DockBadge.swift +++ b/Evergreen/DockBadge.swift @@ -15,16 +15,12 @@ import RSCore func update() { - performSelectorCoalesced(#selector(updateBadge), with: nil, delay: 0.01) + CoalescingQueue.standard.add(self, #selector(updateBadge)) } - @objc dynamic func updateBadge() { + @objc func updateBadge() { - guard let appDelegate = appDelegate else { - return - } - - let unreadCount = appDelegate.unreadCount + let unreadCount = appDelegate?.unreadCount ?? 0 let label = unreadCount > 0 ? "\(unreadCount)" : "" NSApplication.shared.dockTile.badgeLabel = label } diff --git a/Evergreen/FeedList/FeedList.storyboard b/Evergreen/FeedList/FeedList.storyboard index a46b0f412..480c7b595 100644 --- a/Evergreen/FeedList/FeedList.storyboard +++ b/Evergreen/FeedList/FeedList.storyboard @@ -1,7 +1,7 @@ - + - + @@ -9,12 +9,12 @@ - + - + - + @@ -22,118 +22,35 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - + + - + - - + + - - + + - + @@ -147,11 +64,11 @@ - + - + @@ -165,7 +82,7 @@ - + @@ -212,114 +129,61 @@ + + + + + + + + + + + + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/Evergreen/FeedList/FeedListFeed.swift b/Evergreen/FeedList/FeedListFeed.swift index 017f32bfb..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) { @@ -62,15 +60,17 @@ final class FeedListFeed: Hashable, DisplayNameProvider { func downloadIfNeeded() { - guard let lastDownloadAttemptDate = lastDownloadAttemptDate else { - downloadFeed() - return - } + // Not doing feed previews until after 1.0. - let cutoffDate = Date().addingTimeInterval(-(30 * 60)) // 30 minutes in the past - if lastDownloadAttemptDate < cutoffDate { - downloadFeed() - } +// guard let lastDownloadAttemptDate = lastDownloadAttemptDate else { +// downloadFeed() +// return +// } +// +// let cutoffDate = Date().addingTimeInterval(-(30 * 60)) // 30 minutes in the past +// if lastDownloadAttemptDate < cutoffDate { +// downloadFeed() +// } } static func ==(lhs: FeedListFeed, rhs: FeedListFeed) -> Bool { @@ -83,29 +83,29 @@ private extension FeedListFeed { func postFeedListFeedDidBecomeAvailableNotification() { - NotificationCenter.default.post(name: .FeedListFeedDidBecomeAvailable, object: self, userInfo: nil) +// NotificationCenter.default.post(name: .FeedListFeedDidBecomeAvailable, object: self, userInfo: nil) } func downloadFeed() { - lastDownloadAttemptDate = Date() - guard let feedURL = URL(string: url) else { - return - } - - downloadUsingCache(feedURL) { (data, response, error) in - - guard let data = data, error == nil else { - return - } - - let parserData = ParserData(url: self.url, data: data) - FeedParser.parse(parserData) { (parsedFeed, error) in - - if let parsedFeed = parsedFeed, parsedFeed.items.count > 0 { - self.parsedFeed = parsedFeed - } - } - } +// lastDownloadAttemptDate = Date() +// guard let feedURL = URL(string: url) else { +// return +// } +// +// downloadUsingCache(feedURL) { (data, response, error) in +// +// guard let data = data, error == nil else { +// return +// } +// +// let parserData = ParserData(url: self.url, data: data) +// FeedParser.parse(parserData) { (parsedFeed, error) in +// +// if let parsedFeed = parsedFeed, parsedFeed.items.count > 0 { +// self.parsedFeed = parsedFeed +// } +// } +// } } } 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/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 b885478ab..b925e01f5 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,23 @@ final class FeedListViewController: NSViewController { } } +// MARK: Actions + +extension FeedListViewController { + + @IBAction func openHomePage(_ sender: Any?) { + + guard let homePageURL = singleSelectedHomePageURL() else { + return + } + Browser.open(homePageURL, inBackground: false) + } + + @IBAction func addToFeeds(_ sender: Any?) { + + } +} + // MARK: - NSOutlineViewDataSource extension FeedListViewController: NSOutlineViewDataSource { @@ -92,6 +124,8 @@ extension FeedListViewController: NSOutlineViewDelegate { func outlineViewSelectionDidChange(_ notification: Notification) { + updateUI() + let selectedRow = self.outlineView.selectedRow if selectedRow < 0 || selectedRow == NSNotFound { @@ -103,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 @@ -122,7 +159,7 @@ extension FeedListViewController: NSOutlineViewDelegate { if let image = appDelegate.faviconDownloader.favicon(withHomePageURL: feed.homePageURL) { return image } - return appDelegate.genericFeedImage + return AppImages.genericFeedImage } return nil } @@ -134,9 +171,6 @@ extension FeedListViewController: NSOutlineViewDelegate { } return "" } -} - -private extension FeedListViewController { func nodeForRow(_ row: Int) -> Node? { @@ -176,4 +210,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 e4e2f215c..c1a09c961 100644 --- a/Evergreen/FeedList/FeedListWindowController.swift +++ b/Evergreen/FeedList/FeedListWindowController.swift @@ -10,17 +10,12 @@ import AppKit class FeedListWindowController : NSWindowController { - override func windowDidLoad() { - } - - @IBAction func addToFeeds(_ sender: AnyObject) { - - } - - @IBAction func openHomePage(_ sender: AnyObject) { +// window!.appearance = NSAppearance(named: .vibrantDark) + let windowAutosaveName = NSWindow.FrameAutosaveName(rawValue: "FeedDirectoryWindow") + window?.setFrameUsingName(windowAutosaveName, force: true) } } 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 diff --git a/Evergreen/Info.plist b/Evergreen/Info.plist index 7100a1767..511f4486c 100644 --- a/Evergreen/Info.plist +++ b/Evergreen/Info.plist @@ -17,13 +17,13 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0d35 + 1.0d40 CFBundleVersion 522 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 diff --git a/Evergreen/Inspector/FeedInspectorViewController.swift b/Evergreen/Inspector/FeedInspectorViewController.swift index 4214bda61..b112fb4bd 100644 --- a/Evergreen/Inspector/FeedInspectorViewController.swift +++ b/Evergreen/Inspector/FeedInspectorViewController.swift @@ -112,7 +112,7 @@ private extension FeedInspectorViewController { return } - imageView?.image = appDelegate.genericFeedImage + imageView?.image = AppImages.genericFeedImage } func updateName() { diff --git a/Evergreen/Inspector/InspectorWindowController.swift b/Evergreen/Inspector/InspectorWindowController.swift index 28588447d..c350b6b18 100644 --- a/Evergreen/Inspector/InspectorWindowController.swift +++ b/Evergreen/Inspector/InspectorWindowController.swift @@ -32,12 +32,6 @@ final class InspectorWindowController: NSWindowController { } } - var isOpen: Bool { - get { - return isWindowLoaded && window!.isVisible - } - } - private var inspectors: [InspectorViewController]! private var currentInspector: 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 44e8000ef..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! @@ -87,12 +85,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 +107,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/Detail/DetailViewController.swift b/Evergreen/MainWindow/Detail/DetailViewController.swift index 64a6c82b9..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,27 +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") - layer.backgroundColor = color.cgColor - didConfigureLayer = true - } + noSelectionLabel.isHidden = false + multipleSelectionLabel.isHidden = true } } +// MARK: - + private struct ScrollInfo { let contentHeight: CGFloat diff --git a/Evergreen/MainWindow/MainWindowController.swift b/Evergreen/MainWindow/MainWindowController.swift index 5a73960dd..bc9617c6e 100644 --- a/Evergreen/MainWindow/MainWindowController.swift +++ b/Evergreen/MainWindow/MainWindowController.swift @@ -9,25 +9,16 @@ import AppKit import Data import Account - -private let kWindowFrameKey = "MainWindow" +import RSCore class MainWindowController : NSWindowController, NSUserInterfaceValidations { - var isOpen: Bool { - return isWindowLoaded && window!.isVisible - } + @IBOutlet var toolbarDelegate: MainWindowToolbarDelegate? + private let sharingServicePickerDelegate = MainWindowSharingServicePickerDelegate() - var isDisplayingSheet: Bool { - if let _ = window?.attachedSheet { - return true - } - return false - } + private let windowAutosaveName = NSWindow.FrameAutosaveName(rawValue: "MainWindow") + static var didPositionWindowOnFirstRun = false - // MARK: NSWindowController - - private let windowAutosaveName = NSWindow.FrameAutosaveName(rawValue: kWindowFrameKey) private var unreadCount: Int = 0 { didSet { if unreadCount != oldValue { @@ -36,7 +27,11 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } } - static var didPositionWindowOnFirstRun = false + private var shareToolbarItem: NSToolbarItem? { + return window?.toolbar?.existingItem(withIdentifier: .Share) + } + + // MARK: - NSWindowController override func windowDidLoad() { @@ -62,8 +57,6 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { NotificationCenter.default.addObserver(self, selector: #selector(applicationWillTerminate(_:)), name: NSApplication.willTerminateNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(appNavigationKeyPressed(_:)), name: .AppNavigationKeyPressed, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshDidBegin, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshDidFinish, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) @@ -75,40 +68,30 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } } - // MARK: Sidebar + // MARK: - API + + func saveState() { + + // TODO: save width of split view and anything else that should be saved. + + + } func selectedObjectsInSidebar() -> [AnyObject]? { return sidebarViewController?.selectedObjects } - // MARK: Notifications + // MARK: - Notifications @objc func applicationWillTerminate(_ note: Notification) { window?.saveFrame(usingName: windowAutosaveName) } - @objc func appNavigationKeyPressed(_ note: Notification) { - - guard let navigationKey = note.userInfo?[UserInfoKey.navigationKeyPressed] as? Int else { - return - } - guard let contentView = window?.contentView, let view = note.object as? NSView, view.isDescendant(of: contentView) else { - return - } - - if navigationKey == NSRightArrowFunctionKey { - handleRightArrowFunctionKey(in: view) - } - if navigationKey == NSLeftArrowFunctionKey { - handleLeftArrowFunctionKey(in: view) - } - } - @objc func refreshProgressDidChange(_ note: Notification) { - - performSelectorCoalesced(#selector(MainWindowController.makeToolbarValidate(_:)), with: nil, delay: 0.1) + + CoalescingQueue.standard.add(self, #selector(makeToolbarValidate)) } @objc func unreadCountDidChange(_ note: Notification) { @@ -118,14 +101,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } } - // MARK: Toolbar + // MARK: - Toolbar - @objc func makeToolbarValidate(_ sender: Any?) { + @objc func makeToolbarValidate() { window?.toolbar?.validateVisibleItems() } - // MARK: NSUserInterfaceValidations + // MARK: - NSUserInterfaceValidations public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { @@ -145,6 +128,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { return canMarkRead() } + if item.action == #selector(toggleStarred(_:)) { + return validateToggleStarred(item) + } + if item.action == #selector(markOlderArticlesAsRead(_:)) { return canMarkOlderArticlesAsRead() } @@ -185,13 +172,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) } @@ -208,14 +194,6 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { openArticleInBrowser(sender) } - func makeTimelineViewFirstResponder() { - - guard let window = window, let timelineViewController = timelineViewController else { - return - } - window.makeFirstResponderUnlessDescendantIsFirstResponder(timelineViewController.tableView) - } - @IBAction func nextUnread(_ sender: Any?) { guard let timelineViewController = timelineViewController, let sidebarViewController = sidebarViewController else { @@ -233,18 +211,6 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } } - func goToNextUnreadInTimeline() { - - guard let timelineViewController = timelineViewController else { - return - } - - if timelineViewController.canGoToNextUnread() { - timelineViewController.goToNextUnread() - makeTimelineViewFirstResponder() - } - } - @IBAction func markAllAsRead(_ sender: Any?) { timelineViewController?.markAllAsRead() @@ -260,6 +226,11 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { timelineViewController?.markSelectedArticlesAsUnread(sender) } + @IBAction func toggleStarred(_ sender: Any?) { + + timelineViewController?.toggleStarredStatusForSelectedArticles() + } + @IBAction func markAllAsReadAndGoToNextUnread(_ sender: Any?) { markAllAsRead(sender) @@ -340,67 +311,12 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { let items = selectedArticles.map { ArticlePasteboardWriter(article: $0) } let sharingServicePicker = NSSharingServicePicker(items: items) - sharingServicePicker.delegate = self + sharingServicePicker.delegate = sharingServicePickerDelegate sharingServicePicker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY) } - private func canShowShareMenu() -> Bool { - - guard let selectedArticles = selectedArticles else { - return false - } - return !selectedArticles.isEmpty - } } -// MARK: - NSSharingServicePickerDelegate - -extension MainWindowController: NSSharingServicePickerDelegate { - - func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] { - - let sendToServices = appDelegate.sendToCommands.compactMap { (sendToCommand) -> NSSharingService? in - - guard let object = items.first else { - return nil - } - guard sendToCommand.canSendObject(object, selectedText: nil) else { - return nil - } - - let image = sendToCommand.image ?? appDelegate.genericFeedImage ?? NSImage() - return NSSharingService(title: sendToCommand.title, image: image, alternateImage: nil) { - sendToCommand.sendObject(object, selectedText: nil) - } - } - return proposedServices + sendToServices - } -} - -// MARK: - NSToolbarDelegate - -extension NSToolbarItem.Identifier { - static let Share = NSToolbarItem.Identifier("share") -} - -extension MainWindowController: NSToolbarDelegate { - - func toolbarWillAddItem(_ notification: Notification) { - - // The share button should send its action on mouse down, not mouse up. - - guard let item = notification.userInfo?["item"] as? NSToolbarItem else { - return - } - guard item.itemIdentifier == .Share, let button = item.view as? NSButton else { - return - } - - button.sendAction(on: .leftMouseDown) - } -} - - // MARK: - Scripting Access /* @@ -468,6 +384,8 @@ private extension MainWindowController { return oneSelectedArticle?.preferredLink } + // MARK: - Command Validation + func canGoToNextUnread() -> Bool { guard let timelineViewController = timelineViewController, let sidebarViewController = sidebarViewController else { @@ -492,6 +410,70 @@ private extension MainWindowController { return timelineViewController?.canMarkOlderArticlesAsRead() ?? false } + func canShowShareMenu() -> Bool { + + guard let selectedArticles = selectedArticles else { + return false + } + return !selectedArticles.isEmpty + } + + func validateToggleStarred(_ item: NSValidatedUserInterfaceItem) -> Bool { + + let validationStatus = timelineViewController?.markStarredCommandStatus() ?? .canDoNothing + let starring: Bool + let result: Bool + + switch validationStatus { + case .canMark: + starring = true + result = true + case .canUnmark: + starring = false + result = true + case .canDoNothing: + starring = true + result = false + } + + let commandName = starring ? NSLocalizedString("Mark as Starred", comment: "Command") : NSLocalizedString("Mark as Unstarred", comment: "Command") + + if let toolbarItem = item as? NSToolbarItem { + toolbarItem.toolTip = commandName + if let button = toolbarItem.view as? NSButton { + button.image = NSImage(named: starring ? .star : .unstar) + } + } + + if let menuItem = item as? NSMenuItem { + menuItem.title = commandName + } + + return result + } + + // MARK: - Misc. + + func goToNextUnreadInTimeline() { + + guard let timelineViewController = timelineViewController else { + return + } + + if timelineViewController.canGoToNextUnread() { + timelineViewController.goToNextUnread() + makeTimelineViewFirstResponder() + } + } + + func makeTimelineViewFirstResponder() { + + guard let window = window, let timelineViewController = timelineViewController else { + return + } + window.makeFirstResponderUnlessDescendantIsFirstResponder(timelineViewController.tableView) + } + func updateWindowTitle() { if unreadCount < 1 { @@ -501,42 +483,5 @@ private extension MainWindowController { window?.title = "\(appDelegate.appName!) (\(unreadCount))" } } - - // MARK: - Toolbar - - private var shareToolbarItem: NSToolbarItem? { - return existingToolbarItem(identifier: .Share) - } - - func existingToolbarItem(identifier: NSToolbarItem.Identifier) -> NSToolbarItem? { - - guard let toolbarItems = window?.toolbar?.items else { - return nil - } - for toolbarItem in toolbarItems { - if toolbarItem.itemIdentifier == identifier { - return toolbarItem - } - } - return nil - } - - // MARK: - Navigation - - func handleRightArrowFunctionKey(in view: NSView) { - - guard let outlineView = sidebarViewController?.outlineView, view === outlineView, let timelineViewController = timelineViewController else { - return - } - timelineViewController.focus() - } - - func handleLeftArrowFunctionKey(in view: NSView) { - - guard let timelineView = timelineViewController?.tableView, view === timelineView, let sidebarViewController = sidebarViewController else { - return - } - sidebarViewController.focus() - } } diff --git a/Evergreen/MainWindow/MainWindowSharingServicePickerDelegate.swift b/Evergreen/MainWindow/MainWindowSharingServicePickerDelegate.swift new file mode 100644 index 000000000..d598126a3 --- /dev/null +++ b/Evergreen/MainWindow/MainWindowSharingServicePickerDelegate.swift @@ -0,0 +1,31 @@ +// +// MainWindowSharingServicePickerDelegate.swift +// Evergreen +// +// Created by Brent Simmons on 2/17/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit + +@objc final class MainWindowSharingServicePickerDelegate: NSObject, NSSharingServicePickerDelegate { + + func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] { + + let sendToServices = appDelegate.sendToCommands.compactMap { (sendToCommand) -> NSSharingService? in + + guard let object = items.first else { + return nil + } + guard sendToCommand.canSendObject(object, selectedText: nil) else { + return nil + } + + let image = sendToCommand.image ?? AppImages.genericFeedImage ?? NSImage() + return NSSharingService(title: sendToCommand.title, image: image, alternateImage: nil) { + sendToCommand.sendObject(object, selectedText: nil) + } + } + return proposedServices + sendToServices + } +} diff --git a/Evergreen/MainWindow/MainWindowSplitView.swift b/Evergreen/MainWindow/MainWindowSplitView.swift index e7e21ca97..b18fb7752 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) - + + private let splitViewDividerColor = NSColor(calibratedWhite: 0.60, alpha: 1.0) + override var dividerColor: NSColor { - get { - return splitViewDividerColor - } + return splitViewDividerColor } } diff --git a/Evergreen/MainWindow/MainWindowToolbarDelegate.swift b/Evergreen/MainWindow/MainWindowToolbarDelegate.swift new file mode 100644 index 000000000..208969094 --- /dev/null +++ b/Evergreen/MainWindow/MainWindowToolbarDelegate.swift @@ -0,0 +1,30 @@ +// +// MainWindowToolbarDelegate.swift +// Evergreen +// +// Created by Brent Simmons on 2/17/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit + +extension NSToolbarItem.Identifier { + static let Share = NSToolbarItem.Identifier("share") +} + +@objc final class MainWindowToolbarDelegate: NSObject, NSToolbarDelegate { + + func toolbarWillAddItem(_ notification: Notification) { + + // The share button should send its action on mouse down, not mouse up. + + guard let item = notification.userInfo?["item"] as? NSToolbarItem else { + return + } + guard item.itemIdentifier == .Share, let button = item.view as? NSButton else { + return + } + + button.sendAction(on: .leftMouseDown) + } +} 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/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/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/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/SidebarStatusBarView.swift b/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift index 0da539ef1..dbd4df147 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarStatusBarView.swift @@ -17,18 +17,27 @@ final class SidebarStatusBarView: NSView { @IBOutlet var progressIndicator: NSProgressIndicator! @IBOutlet var progressLabel: NSTextField! + private var didConfigureLayer = false + private var isAnimatingProgress = false { didSet { progressIndicator.isHidden = !isAnimatingProgress progressLabel.isHidden = !isAnimatingProgress } } - - override var isFlipped: Bool { - get { - return true + + private var progress: CombinedRefreshProgress? = nil { + didSet { + CoalescingQueue.standard.add(self, #selector(updateUI)) } } + override var isFlipped: Bool { + return true + } + + override var wantsUpdateLayer: Bool { + return true + } override func awakeFromNib() { @@ -42,28 +51,34 @@ final class SidebarStatusBarView: NSView { NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) } - // MARK: Notifications + override func updateLayer() { - @objc dynamic func progressDidChange(_ notification: Notification) { + 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 { + stopProgressIfNeeded() + return + } - let progress = AccountManager.shared.combinedRefreshProgress updateProgressIndicator(progress) updateProgressLabel(progress) } - // MARK: Drawing + // MARK: Notifications -// private let lineColor = NSColor(calibratedWhite: 0.57, alpha: 1.0) -// -// override func draw(_ dirtyRect: NSRect) { -// -// let path = NSBezierPath() -// path.lineWidth = 1.0 -// path.move(to: NSPoint(x: NSMinX(bounds), y: NSMinY(bounds) + 0.5)) -// path.line(to: NSPoint(x: NSMaxX(bounds), y: NSMinY(bounds) + 0.5)) -// lineColor.set() -// path.stroke() -// } + @objc dynamic func progressDidChange(_ notification: Notification) { + + progress = AccountManager.shared.combinedRefreshProgress + } } private extension SidebarStatusBarView { 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/Sidebar/SidebarViewController.swift b/Evergreen/MainWindow/Sidebar/SidebarViewController.swift index 3afc39974..2a035aab7 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! @@ -32,12 +36,13 @@ import RSCore return selectedNodes.representedObjects() } - //MARK: NSViewController + // MARK: - NSViewController override func viewDidLoad() { sidebarCellAppearance = SidebarCellAppearance(theme: appDelegate.currentTheme, fontSize: AppDefaults.shared.sidebarFontSize) + outlineView.dataSource = dataSource outlineView.setDraggingSourceOperationMask(.move, forLocal: true) outlineView.setDraggingSourceOperationMask(.copy, forLocal: false) @@ -65,9 +70,9 @@ import RSCore } } - //MARK: Notifications + // MARK: - Notifications - @objc dynamic func unreadCountDidChange(_ note: Notification) { + @objc func unreadCountDidChange(_ note: Notification) { guard let representedObject = note.object else { return @@ -75,17 +80,17 @@ import RSCore configureUnreadCountForCellsForRepresentedObject(representedObject as AnyObject) } - @objc dynamic func containerChildrenDidChange(_ note: Notification) { + @objc func containerChildrenDidChange(_ note: Notification) { rebuildTreeAndReloadDataIfNeeded() } - @objc dynamic func batchUpdateDidPerform(_ notification: Notification) { + @objc func batchUpdateDidPerform(_ notification: Notification) { rebuildTreeAndReloadDataIfNeeded() } - @objc dynamic func userDidAddFeed(_ notification: Notification) { + @objc func userDidAddFeed(_ notification: Notification) { guard let feed = notification.userInfo?[UserInfoKey.feed] else { return @@ -114,7 +119,7 @@ import RSCore configureCellsForRepresentedObject(object as AnyObject) } - // MARK: Actions + // MARK: - Actions @IBAction func delete(_ sender: AnyObject?) { @@ -165,11 +170,16 @@ import RSCore outlineView.revealAndSelectRepresentedObject(SmartFeedsController.shared.starredFeed, treeController) } - // MARK: Navigation + @IBAction func copy(_ sender: Any?) { + + NSPasteboard.general.copyObjects(selectedObjects) + } + + // MARK: - Navigation func canGoToNextUnread() -> Bool { - if let _ = rowContainingNextUnread() { + if let _ = nextSelectableRowWithUnreadArticle() { return true } return false @@ -177,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 } @@ -193,7 +203,7 @@ import RSCore window.makeFirstResponderUnlessDescendantIsFirstResponder(outlineView) } - // MARK: Contextual Menu + // MARK: - Contextual Menu func contextualMenuForSelectedObjects() -> NSMenu? { @@ -216,7 +226,7 @@ import RSCore return menu(for: [object]) } - // MARK: NSOutlineViewDelegate + // MARK: - NSOutlineViewDelegate func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { @@ -262,44 +272,20 @@ 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) } +} - // MARK: NSOutlineViewDataSource +// MARK: - NSUserInterfaceValidations - 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 - } +extension SidebarViewController: NSUserInterfaceValidations { - func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { + func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { - let node = nodeForItem(item as AnyObject?) - if let feed = node.representedObject as? Feed { - return FeedPasteboardWriter(feed: feed) + if item.action == #selector(copy(_:)) { + return NSPasteboard.general.canCopyAtLeastOneObject(selectedObjects) } - return nil + return true } } @@ -394,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 @@ -409,7 +405,7 @@ private extension SidebarViewController { row = 0 while (row <= selectedRow) { - if rowHasAtLeastOneUnreadArticle(row) { + if rowHasAtLeastOneUnreadArticle(row) && !rowIsGroupItem(row) { return row } row += 1 @@ -574,20 +570,4 @@ private extension SidebarViewController { } } -extension Feed: SmallIconProvider { - - var smallIcon: NSImage? { - if let image = appDelegate.faviconDownloader.favicon(for: self) { - return image - } - return appDelegate.genericFeedImage - } -} - -extension Folder: SmallIconProvider { - - var smallIcon: NSImage? { - return NSImage(named: NSImage.Name.folder) - } -} 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/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/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/Cell/TimelineCellAppearance.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift index 875ff7c10..01e4d8bee 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellAppearance.swift @@ -26,15 +26,22 @@ struct TimelineCellAppearance: Equatable { let textColor: NSColor let textFont: NSFont - + + let textOnlyColor: NSColor + let textOnlyFont: NSFont + let unreadCircleColor: NSColor let unreadCircleDimension: CGFloat let unreadCircleMarginRight: CGFloat - + + let starDimension: CGFloat + let gridColor: NSColor + let drawsGrid: Bool let avatarSize: NSSize let avatarMarginRight: CGFloat + let avatarMarginLeft: CGFloat let avatarAdjustmentTop: CGFloat let avatarCornerRadius: CGFloat let showAvatar: Bool @@ -44,40 +51,48 @@ 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") - self.titleFont = NSFont.systemFont(ofSize: actualFontSize, weight: NSFont.Weight.bold) + 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 = 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") + + 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.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/TimelineCellData.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift index 314a52d4f..7e70e34ad 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellData.swift @@ -27,9 +27,10 @@ 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?) { - + self.title = timelineTruncatedTitle(article) self.text = timelineTruncatedSummary(article) @@ -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() { @@ -114,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.textOnlyFont]) } diff --git a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift index decf3dffc..a53a5bfe2 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineCellLayout.swift @@ -18,132 +18,151 @@ struct TimelineCellLayout { let dateRect: NSRect let titleRect: NSRect let unreadIndicatorRect: NSRect + let starRect: NSRect let avatarImageRect: NSRect let paddingBottom: CGFloat - init(width: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, unreadIndicatorRect: NSRect, avatarImageRect: NSRect, paddingBottom: CGFloat) { + init(width: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, avatarImageRect: NSRect, paddingBottom: CGFloat) { self.width = width self.feedNameRect = feedNameRect self.dateRect = dateRect self.titleRect = titleRect self.unreadIndicatorRect = unreadIndicatorRect + 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 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 -//} + var r = textBoxRect + let renderer = RSMultiLineRenderer(attributedTitle: cellData.attributedTitle) -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 + let measurements = renderer.measurements(forWidth: textBoxRect.width) + r.size.height = CGFloat(measurements.height) - let textWidth = width - (r.origin.x + appearance.cellPadding.right) - let renderer = RSMultiLineRenderer(attributedTitle: cellData.attributedTitle) + var rline1 = r + rline1.size.height = CGFloat(measurements.heightOfFirstLine) - let measurements = renderer.measurements(forWidth: textWidth) - r.size = NSSize(width: textWidth, height: CGFloat(measurements.height)) - r.size.width = max(r.size.width, 0.0) + return (r, rline1) + } - var rline1 = r - rline1.size.height = CGFloat(measurements.heightOfFirstLine) - - return (r, rline1) -} + static func rectForDate(_ textBoxRect: NSRect, _ titleRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { -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) - - return r -} + return rectOfLineBelow(textBoxRect, titleRect, appearance.titleBottomMargin, cellData.attributedDateString) + } -private func rectForAvatar(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ titleLine1Rect: NSRect) -> NSRect { + static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> 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 faviconRect = rectForFavicon(cellData, appearance, feedNameRect, unreadIndicatorRect) - let avatarImageRect = rectForAvatar(cellData, appearance, titleLine1Rect) + func maxY() -> CGFloat { - return TimelineCellLayout(width: width, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, unreadIndicatorRect: unreadIndicatorRect, 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 336dda0d7..a7971a06d 100644 --- a/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Evergreen/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -11,27 +11,28 @@ import RSTextDrawing 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 imageView.imageAlignment = .alignCenter - imageView.image = appDelegate.genericFeedImage + imageView.image = AppImages.genericFeedImage 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 -// }() + private 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 { @@ -77,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(faviconImageView, hidden: true) - } - override init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -122,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) @@ -140,7 +119,7 @@ class TimelineTableCellView: NSTableCellView { dateView.rs_setFrameIfNotEqual(layoutRects.dateRect) feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect) avatarImageView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect) -// faviconImageView.rs_setFrameIfNotEqual(layoutRects.faviconRect) + starView.rs_setFrameIfNotEqual(layoutRects.starRect) } override func updateLayer() { @@ -157,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(width: bounds.width, 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 { @@ -185,14 +203,20 @@ class TimelineTableCellView: NSTableCellView { } } - private func updateUnreadIndicator() { - - if unreadIndicatorView.isHidden != cellData.read { - unreadIndicatorView.isHidden = cellData.read + func updateUnreadIndicator() { + + let shouldHide = cellData.read || cellData.starred + if unreadIndicatorView.isHidden != shouldHide { + unreadIndicatorView.isHidden = shouldHide } } - private func updateAvatar() { + func updateStarView() { + + starView.isHidden = !cellData.starred + } + + func updateAvatar() { if !cellData.showAvatar { avatarImageView.image = nil @@ -213,46 +237,15 @@ 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() { - -// 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() updateFeedNameView() updateUnreadIndicator() + updateStarView() updateAvatar() -// updateFavicon() - } - - private func updateAppearance() { - - if let rowView = superview as? NSTableRowView { - isEmphasized = rowView.isEmphasized - isSelected = rowView.isSelected - } - else { - isEmphasized = false - isSelected = false - } } } diff --git a/Evergreen/MainWindow/Timeline/TimelineDataSource.swift b/Evergreen/MainWindow/Timeline/TimelineDataSource.swift new file mode 100644 index 000000000..620c457fe --- /dev/null +++ b/Evergreen/MainWindow/Timeline/TimelineDataSource.swift @@ -0,0 +1,32 @@ +// +// TimelineDataSource.swift +// Evergreen +// +// Created by Brent Simmons on 2/17/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit + +@objc final class TimelineDataSource: NSObject, NSTableViewDataSource { + + var articles = ArticleArray() + + func numberOfRows(in tableView: NSTableView) -> Int { + + return articles.count + } + + func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { + + return articles.articleAtRow(row) ?? nil + } + + func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? { + + guard let article = articles.articleAtRow(row) else { + return nil + } + return ArticlePasteboardWriter(article: article) + } +} diff --git a/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift b/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift index 772396041..03ed5b125 100644 --- a/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift +++ b/Evergreen/MainWindow/Timeline/TimelineTableRowView.swift @@ -17,20 +17,18 @@ class TimelineTableRowView : NSTableRowView { } } } - + // override var interiorBackgroundStyle: NSBackgroundStyle { // return .Light // } - + 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,12 +48,9 @@ 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) { let path = NSBezierPath() @@ -73,7 +68,7 @@ class TimelineTableRowView : NSTableRowView { super.draw(dirtyRect) - if !isSelected && !isNextRowSelected { + if cellAppearance.drawsGrid && !isSelected && !isNextRowSelected { drawSeparator(in: dirtyRect) } } diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift index 3a0df2348..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 = MarkReadOrUnreadCommand(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()) } diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 993a8d8ba..2d1ea4635 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -16,23 +16,20 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { @IBOutlet var tableView: TimelineTableView! @IBOutlet var contextualMenuDelegate: TimelineContextualMenuDelegate? - + @IBOutlet var dataSource: TimelineDataSource! + 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() { didSet { if articles != oldValue { - clearUndoableCommands() + dataSource.articles = articles updateShowAvatars() tableView.reloadData() } @@ -42,7 +39,6 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { var undoableCommands = [UndoableCommand]() private var cellAppearance: TimelineCellAppearance! private var cellAppearanceWithAvatar: TimelineCellAppearance! - private var showFeedNames = false { didSet { if showFeedNames != oldValue { @@ -61,8 +57,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { } private var didRegisterForNotifications = false - private var reloadAvailableCellsTimer: Timer? - private var fetchAndMergeArticlesTimer: Timer? + static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) private var sortDirection = AppDefaults.shared.timelineSortDirection { didSet { @@ -106,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() { @@ -157,7 +150,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) @@ -175,14 +168,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 @@ -198,10 +191,10 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { markSelectedArticlesAsUnread(sender) } } - + @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,12 +202,49 @@ 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) } + @IBAction func copy(_ sender: Any?) { + + NSPasteboard.general.copyObjects(selectedArticles) + } + + func toggleStarredStatusForSelectedArticles() { + + // If any one of the selected articles is not starred, then star them. + // If all articles are starred, then unstar them. + + let commandStatus = markStarredCommandStatus() + let starring: Bool + switch commandStatus { + case .canMark: + starring = true + case .canUnmark: + starring = false + case .canDoNothing: + return + } + + guard let undoManager = undoManager, let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles, markingStarred: starring, undoManager: undoManager) else { + return + } + runCommand(markStarredCommand) + } + + func markStarredCommandStatus() -> MarkCommandValidationStatus { + + return MarkCommandValidationStatus.statusFor(selectedArticles) { $0.anyArticleIsUnstarred() } + } + + func markReadCommandStatus() -> MarkCommandValidationStatus { + + return MarkCommandValidationStatus.statusFor(selectedArticles) { $0.anyArticleIsUnread() } + } + func markOlderArticlesAsRead() { // Mark articles the same age or older than the selected article(s) as read. @@ -237,7 +267,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) @@ -411,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 } @@ -421,28 +451,40 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { rowHeightWithoutFeedName = calculateRowHeight(showingFeedNames: false) updateTableViewRowHeight() } + + @objc func fetchAndMergeArticles() { + + guard let representedObjects = representedObjects else { + return + } + + performBlockAndRestoreSelection { + + var unsortedArticles = fetchUnsortedArticles(for: representedObjects) + + // Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles. + let unsortedArticleIDs = unsortedArticles.articleIDs() + for article in articles { + if !unsortedArticleIDs.contains(article.articleID) { + unsortedArticles.insert(article) + } + } + + updateArticles(with: unsortedArticles) + } + } } -// MARK: - NSTableViewDataSource +// MARK: NSUserInterfaceValidations -extension TimelineViewController: NSTableViewDataSource { +extension TimelineViewController: NSUserInterfaceValidations { - func numberOfRows(in tableView: NSTableView) -> Int { + func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { - return articles.count - } - - func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { - - return articles.articleAtRow(row) ?? nil - } - - func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? { - - guard let article = articles.articleAtRow(row) else { - return nil + if item.action == #selector(copy(_:)) { + return NSPasteboard.general.canCopyAtLeastOneObject(selectedArticles) } - return ArticlePasteboardWriter(article: article) + return true } } @@ -474,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 @@ -535,9 +575,6 @@ extension TimelineViewController: NSTableViewDelegate { return nil } - // TODO: make Feed know about its authors. - // https://github.com/brentsimmons/Evergreen/issues/212 - return appDelegate.feedIconDownloader.icon(for: feed) } @@ -565,7 +602,7 @@ extension TimelineViewController: NSTableViewDelegate { private extension TimelineViewController { - func reloadAvailableCells() { + @objc func reloadAvailableCells() { if let indexesToReload = tableView.indexesOfAvailableRows() { reloadCells(for: indexesToReload) @@ -574,21 +611,7 @@ private extension TimelineViewController { func queueReloadAvailableCells() { - invalidateReloadTimer() - reloadAvailableCellsTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { (timer) in - self.reloadAvailableCells() - self.invalidateReloadTimer() - } - } - - func invalidateReloadTimer() { - - if let timer = reloadAvailableCellsTimer { - if timer.isValid { - timer.invalidate() - } - reloadAvailableCellsTimer = nil - } + CoalescingQueue.standard.add(self, #selector(reloadAvailableCells)) } func updateTableViewRowHeight() { @@ -687,19 +710,6 @@ private extension TimelineViewController { return fetchedArticles } - func fetchAndMergeArticles() { - - guard let representedObjects = representedObjects else { - return - } - - performBlockAndRestoreSelection { - var unsortedArticles = fetchUnsortedArticles(for: representedObjects) - unsortedArticles.formUnion(Set(articles)) - updateArticles(with: unsortedArticles) - } - } - func selectArticles(_ articleIDs: [String]) { let indexesToSelect = articles.indexesForArticleIDs(Set(articleIDs)) @@ -710,23 +720,9 @@ private extension TimelineViewController { tableView.selectRowIndexes(indexesToSelect, byExtendingSelection: false) } - func invalidateFetchAndMergeArticlesTimer() { - - if let timer = fetchAndMergeArticlesTimer { - if timer.isValid { - timer.invalidate() - } - fetchAndMergeArticlesTimer = nil - } - } - func queueFetchAndMergeArticles() { - invalidateFetchAndMergeArticlesTimer() - fetchAndMergeArticlesTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { (timer) in - self.fetchAndMergeArticles() - self.invalidateFetchAndMergeArticlesTimer() - } + TimelineViewController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) } func representedObjectArraysAreEqual(_ objects1: [AnyObject]?, _ objects2: [AnyObject]?) -> Bool { diff --git a/Evergreen/Preferences/PreferencesWindowController.swift b/Evergreen/Preferences/PreferencesWindowController.swift index ee4b83d94..423808f41 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?) { } @@ -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/Resources/DB5.plist b/Evergreen/Resources/DB5.plist index 8a8a06706..52de2341e 100644 --- a/Evergreen/Resources/DB5.plist +++ b/Evergreen/Resources/DB5.plist @@ -66,6 +66,8 @@ 000000 gridColorAlpha 0.1 + drawsGrid + header backgroundColor @@ -74,15 +76,15 @@ cell paddingLeft - 12 + 20 paddingRight - 12 + 20 paddingTop - 12 + 16 paddingBottom - 14 + 16 feedNameColor - 999999 + aaaaaa faviconFeedNameSpacing 2 dateColor @@ -92,11 +94,13 @@ dateMarginBottom 2 textColor - 666666 + aaaaaa + textOnlyColor + 222222 titleColor 222222 titleMarginBottom - 1 + 2 unreadCircleColor #2db6ff unreadCircleDimension @@ -108,11 +112,15 @@ avatarWidth 48 avatarMarginRight + 20 + avatarMarginLeft 8 avatarAdjustmentTop 4 avatarCornerRadius 7 + starDimension + 13 Detail diff --git a/Evergreen/SmartFeeds/PseudoFeed.swift b/Evergreen/SmartFeeds/PseudoFeed.swift index aca8b0c4a..19098fef6 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 { } @@ -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 99de45209..c7bfa7623 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 { @@ -32,23 +30,31 @@ final class SmartFeed: PseudoFeed { } } + var pasteboardWriter: NSPasteboardWriting { + return SmartFeedPasteboardWriter(smartFeed: self) + } + private let delegate: SmartFeedDelegate - private var timer: Timer? private var unreadCounts = [Account: Int]() init(delegate: SmartFeedDelegate) { self.delegate = delegate NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) - startTimer() // Fetch unread count at startup + queueFetchUnreadCounts() // Fetch unread count at startup } @objc func unreadCountDidChange(_ note: Notification) { if note.object is Account { - startTimer() + queueFetchUnreadCounts() } } + + @objc func fetchUnreadCounts() { + + AccountManager.shared.accounts.forEach { self.fetchUnreadCount(for: $0) } + } } extension SmartFeed: ArticleFetcher { @@ -66,9 +72,12 @@ extension SmartFeed: ArticleFetcher { private extension SmartFeed { - // MARK: - Unread Counts + func queueFetchUnreadCounts() { - private func fetchUnreadCount(for account: Account) { + CoalescingQueue.standard.add(self, #selector(fetchUnreadCounts)) + } + + func fetchUnreadCount(for account: Account) { delegate.fetchUnreadCount(for: account) { (accountUnreadCount) in self.unreadCounts[account] = accountUnreadCount @@ -76,12 +85,7 @@ private extension SmartFeed { } } - private func fetchUnreadCounts() { - - AccountManager.shared.accounts.forEach { self.fetchUnreadCount(for: $0) } - } - - private func updateUnreadCount() { + func updateUnreadCount() { unreadCount = AccountManager.shared.accounts.reduce(0) { (result, account) -> Int in if let oneUnreadCount = unreadCounts[account] { @@ -90,26 +94,4 @@ private extension SmartFeed { return result } } - - // MARK: - Timer - - func stopTimer() { - - if let timer = timer { - timer.rs_invalidateIfValid() - } - timer = nil - } - - private static let fetchCoalescingDelay: TimeInterval = 0.1 - - func startTimer() { - - stopTimer() - - timer = Timer.scheduledTimer(withTimeInterval: SmartFeed.fetchCoalescingDelay, repeats: false, block: { (timer) in - self.fetchUnreadCounts() - self.stopTimer() - }) - } } diff --git a/Evergreen/SmartFeeds/SmartFeedPasteboardWriter.swift b/Evergreen/SmartFeeds/SmartFeedPasteboardWriter.swift new file mode 100644 index 000000000..0d716c9f5 --- /dev/null +++ b/Evergreen/SmartFeeds/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/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/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 diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 44c334222..76d32af9b 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -56,23 +56,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, let database: Database let delegate: AccountDelegate var username: String? - var saveTimer: Timer? + static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0) public var dirty = false { didSet { - - if refreshInProgress { - if let _ = saveTimer { - removeSaveTimer() - } - return - } - - if dirty { - resetSaveTimer() - } - else { - removeSaveTimer() + if dirty && !refreshInProgress { + queueSaveToDiskIfNeeded() } } } @@ -93,28 +82,22 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } else { NotificationCenter.default.post(name: .AccountRefreshDidFinish, object: self) - if dirty { - resetSaveTimer() - } + queueSaveToDiskIfNeeded() } } } } 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() @@ -364,6 +347,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. @@ -469,6 +457,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } + @objc func saveToDiskIfNeeded() { + + guard dirty else { + return + } + saveToDisk() + dirty = false + } + // MARK: - Equatable public class func ==(lhs: Account, rhs: Account) -> Bool { @@ -498,6 +495,11 @@ private extension Account { static let unreadCount = "unreadCount" } + func queueSaveToDiskIfNeeded() { + + Account.saveQueue.add(self, #selector(saveToDiskIfNeeded)) + } + func object(with diskObject: [String: Any]) -> AnyObject? { if Feed.isFeedDictionary(diskObject) { @@ -552,21 +554,6 @@ private extension Account { return d as NSDictionary } - func saveToDiskIfNeeded() { - - if !dirty { - return - } - - if refreshInProgress { - resetSaveTimer() - return - } - - saveToDisk() - dirty = false - } - func saveToDisk() { let d = diskDictionary() @@ -577,21 +564,6 @@ private extension Account { NSApplication.shared.presentError(error) } } - - func resetSaveTimer() { - - saveTimer?.rs_invalidateIfValid() - - saveTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { (timer) in - self.saveToDiskIfNeeded() - } - } - - func removeSaveTimer() { - - saveTimer?.rs_invalidateIfValid() - saveTimer = nil - } } // MARK: - Private 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 85d411b06..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 @@ -58,6 +55,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 @@ -83,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 @@ -130,7 +126,7 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, return true } - // MARK: Notifications + // MARK: - Notifications @objc func unreadCountDidChange(_ note: Notification) { @@ -141,6 +137,11 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, } } + @objc func childrenDidChange(_ note: Notification) { + + updateUnreadCount() + } + // MARK: - Equatable static public func ==(lhs: Folder, rhs: Folder) -> Bool { 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/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) { 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.xcodeproj/project.pbxproj b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj index d5c387606..e408ccf8d 100755 --- a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj +++ b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj @@ -31,7 +31,6 @@ 842DD7CE1E14995C00E061EB /* RSPlist.m in Sources */ = {isa = PBXBuildFile; fileRef = 844C915A1B65753E0051FC1B /* RSPlist.m */; }; 842DD7CF1E14995C00E061EB /* RSMacroProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 8453F7DC1BDF337800B1C8ED /* RSMacroProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 842DD7D01E14995C00E061EB /* RSMacroProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 8453F7DD1BDF337800B1C8ED /* RSMacroProcessor.m */; }; - 842DD7D41E14995C00E061EB /* DiskSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849BF8B91C9130150071D1DA /* DiskSaver.swift */; }; 842DD7D51E14995C00E061EB /* PlistProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A835891D4EC7B80004C598 /* PlistProviderProtocol.swift */; }; 842DD7D61E14996300E061EB /* NSArray+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84CFF5251AC3C9A200CEA6C8 /* NSArray+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 842DD7D71E14996300E061EB /* NSArray+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84CFF5261AC3C9A200CEA6C8 /* NSArray+RSCore.m */; }; @@ -95,14 +94,19 @@ 849A339E1AC90A0A0015BA09 /* NSTableView+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 849A339C1AC90A0A0015BA09 /* NSTableView+RSCore.m */; }; 849B08971BF7BCE30090CEE4 /* NSPasteboard+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 849B08951BF7BCE30090CEE4 /* NSPasteboard+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 849B08981BF7BCE30090CEE4 /* NSPasteboard+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 849B08961BF7BCE30090CEE4 /* NSPasteboard+RSCore.m */; }; - 849BF8BA1C9130150071D1DA /* DiskSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849BF8B91C9130150071D1DA /* DiskSaver.swift */; }; + 849EE70D2039187D0082A1EA /* NSWindowController+RSCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849EE70C2039187D0082A1EA /* NSWindowController+RSCore.swift */; }; + 849EE72320393A750082A1EA /* NSToolbar+RSCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849EE72220393A750082A1EA /* NSToolbar+RSCore.swift */; }; + 849EE72520393AEA0082A1EA /* Array+RSCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849EE72420393AEA0082A1EA /* Array+RSCore.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 */; }; 84B99C9A1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */; }; 84B99C9B1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */; }; 84BB45431D6909C700B48537 /* NSMutableDictionary-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */; }; + 84C326872038C9F6006A025C /* CoalescingQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C326862038C9F6006A025C /* CoalescingQueue.swift */; }; 84C632A0200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84C632A1200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */; }; 84C632A4200D356E007BEEAA /* SendToBlogEditorApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -215,12 +219,17 @@ 849A339C1AC90A0A0015BA09 /* NSTableView+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSTableView+RSCore.m"; sourceTree = ""; }; 849B08951BF7BCE30090CEE4 /* NSPasteboard+RSCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSPasteboard+RSCore.h"; sourceTree = ""; }; 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 = ""; }; + 849EE70C2039187D0082A1EA /* NSWindowController+RSCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSWindowController+RSCore.swift"; path = "AppKit/NSWindowController+RSCore.swift"; sourceTree = ""; }; + 849EE72220393A750082A1EA /* NSToolbar+RSCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSToolbar+RSCore.swift"; path = "AppKit/NSToolbar+RSCore.swift"; sourceTree = ""; }; + 849EE72420393AEA0082A1EA /* Array+RSCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+RSCore.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 = ""; }; 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableDictionary-Extensions.swift"; sourceTree = ""; }; + 84C326862038C9F6006A025C /* CoalescingQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CoalescingQueue.swift; path = RSCore/CoalescingQueue.swift; sourceTree = ""; }; 84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSAppleEventDescriptor+RSCore.h"; path = "AppKit/NSAppleEventDescriptor+RSCore.h"; sourceTree = ""; }; 84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSAppleEventDescriptor+RSCore.m"; path = "AppKit/NSAppleEventDescriptor+RSCore.m"; sourceTree = ""; }; 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SendToBlogEditorApp.h; path = AppKit/SendToBlogEditorApp.h; sourceTree = ""; }; @@ -357,13 +366,14 @@ 8453F7DD1BDF337800B1C8ED /* RSMacroProcessor.m */, 84B99C931FAE64D400ECDEDB /* DisplayNameProvider.swift */, 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */, - 849BF8B91C9130150071D1DA /* DiskSaver.swift */, 84A835891D4EC7B80004C598 /* PlistProviderProtocol.swift */, 842E45CB1ED623C7000A8B52 /* UniqueIdentifier.swift */, 84E34DA51F9FA1070077082F /* UndoableCommand.swift */, 8402047D1FBCE77900D94C1A /* BatchUpdate.swift */, 848F6AE81FC2BC50002D422E /* ThreadSafeCache.swift */, 844B5B561FE9D36000C7C76A /* Keyboard.swift */, + 84AD1EA420315A8700BC20B7 /* PasteboardWriterOwner.swift */, + 84C326862038C9F6006A025C /* CoalescingQueue.swift */, 84CFF5241AC3C8A200CEA6C8 /* Foundation */, 84CFF5551AC3CF4A00CEA6C8 /* AppKit */, 84CFF5661AC3D13F00CEA6C8 /* Images */, @@ -434,6 +444,7 @@ 84CFF54A1AC3CDAC00CEA6C8 /* NSString+RSCore.m */, 84CFF5451AC3CD8000CEA6C8 /* NSTimer+RSCore.h */, 84CFF5461AC3CD8000CEA6C8 /* NSTimer+RSCore.m */, + 849EE72420393AEA0082A1EA /* Array+RSCore.swift */, 84FEB4AB1D19D7F4004727E5 /* Date+Extensions.swift */, 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */, 8414CBA61C95F2EA00333C12 /* Set+Extensions.swift */, @@ -466,9 +477,11 @@ 842635561D7FA1C800196285 /* NSTableView+Extensions.swift */, 849A339B1AC90A0A0015BA09 /* NSTableView+RSCore.h */, 849A339C1AC90A0A0015BA09 /* NSTableView+RSCore.m */, + 849EE72220393A750082A1EA /* NSToolbar+RSCore.swift */, 84CFF5561AC3CF9100CEA6C8 /* NSView+RSCore.h */, 84CFF5571AC3CF9100CEA6C8 /* NSView+RSCore.m */, 8432B1871DACA2060057D6DF /* NSWindow-Extensions.swift */, + 849EE70C2039187D0082A1EA /* NSWindowController+RSCore.swift */, 8414CBA91C95F8F700333C12 /* RSGeometry.h */, 8414CBAA1C95F8F700333C12 /* RSGeometry.m */, 8461387E1DB3F5BE00048B83 /* RSToolbarItem.swift */, @@ -480,6 +493,7 @@ 84C687371FBC028900345C9E /* LogItem.swift */, 8434D15B200BD6F400D6281E /* UserApp.swift */, 84D5BA1D201E87E2009092BD /* URLPasteboardWriter.swift */, + 84AD1EA720315BA900BC20B7 /* NSPasteboard+RSCore.swift */, 842DD7F91E1499FA00E061EB /* Views */, ); name = AppKit; @@ -735,7 +749,6 @@ 842DD7C81E14995C00E061EB /* RSConstants.m in Sources */, 845A29201FC8BC49007B49E3 /* BinaryDiskCache.swift in Sources */, 84C687391FBC028900345C9E /* LogItem.swift in Sources */, - 842DD7D41E14995C00E061EB /* DiskSaver.swift in Sources */, 842DD7E11E14996300E061EB /* NSFileManager+RSCore.m in Sources */, 842DD7C61E14995C00E061EB /* RSBlocks.m in Sources */, 842DD7DD1E14996300E061EB /* NSDate+RSCore.m in Sources */, @@ -765,7 +778,6 @@ files = ( 8417FE031AC67D430048E9B7 /* RSOpaqueContainerView.m in Sources */, 84CFF5481AC3CD8000CEA6C8 /* NSTimer+RSCore.m in Sources */, - 849BF8BA1C9130150071D1DA /* DiskSaver.swift in Sources */, 84FE9FC41C00453900081CE9 /* NSStoryboard+RSCore.m in Sources */, 84CFF5341AC3CB6800CEA6C8 /* NSDictionary+RSCore.m in Sources */, 84CFF54C1AC3CDAC00CEA6C8 /* NSString+RSCore.m in Sources */, @@ -780,6 +792,7 @@ 8414CBAC1C95F8F700333C12 /* RSGeometry.m in Sources */, 84134D201C59D5450063FD24 /* NSCalendar+RSCore.m in Sources */, 84CFF5651AC3D13C00CEA6C8 /* RSImageRenderer.m in Sources */, + 849EE70D2039187D0082A1EA /* NSWindowController+RSCore.swift in Sources */, 84CFF5381AC3CBB200CEA6C8 /* NSMutableArray+RSCore.m in Sources */, 84CFF5401AC3CD0100CEA6C8 /* NSMutableSet+RSCore.m in Sources */, 84CFF5441AC3CD3500CEA6C8 /* NSNotificationCenter+RSCore.m in Sources */, @@ -788,7 +801,9 @@ 84C687321FBAA3DF00345C9E /* LogWindowController.swift in Sources */, 84C687381FBC028900345C9E /* LogItem.swift in Sources */, 8432B1861DACA0E90057D6DF /* NSResponder-Extensions.swift in Sources */, + 849EE72520393AEA0082A1EA /* Array+RSCore.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,13 +825,16 @@ 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 */, 844F91D61D90D86100820C48 /* RSTransparentContainerView.m in Sources */, 84CFF56E1AC3D20A00CEA6C8 /* NSImage+RSCore.m in Sources */, 8453F7DF1BDF337800B1C8ED /* RSMacroProcessor.m in Sources */, + 84C326872038C9F6006A025C /* CoalescingQueue.swift in Sources */, 842E45CC1ED623C7000A8B52 /* UniqueIdentifier.swift in Sources */, + 849EE72320393A750082A1EA /* NSToolbar+RSCore.swift in Sources */, 84A8358A1D4EC7B80004C598 /* PlistProviderProtocol.swift in Sources */, 849A339E1AC90A0A0015BA09 /* NSTableView+RSCore.m in Sources */, 8434D15C200BD6F400D6281E /* UserApp.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/AppKit/NSToolbar+RSCore.swift b/Frameworks/RSCore/RSCore/AppKit/NSToolbar+RSCore.swift new file mode 100644 index 000000000..2024c6227 --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/NSToolbar+RSCore.swift @@ -0,0 +1,17 @@ +// +// NSToolbar+RSCore.swift +// RSCore +// +// Created by Brent Simmons on 2/17/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import AppKit + +public extension NSToolbar { + + public func existingItem(withIdentifier identifier: NSToolbarItem.Identifier) -> NSToolbarItem? { + + return items.firstElementPassingTest{ $0.itemIdentifier == identifier } + } +} diff --git a/Frameworks/RSCore/RSCore/AppKit/NSWindowController+RSCore.swift b/Frameworks/RSCore/RSCore/AppKit/NSWindowController+RSCore.swift new file mode 100644 index 000000000..61cdc263e --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/NSWindowController+RSCore.swift @@ -0,0 +1,22 @@ +// +// NSWindowController+RSCore.swift +// RSCore +// +// Created by Brent Simmons on 2/17/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import AppKit + +public extension NSWindowController { + + public var isDisplayingSheet: Bool { + + return window?.isDisplayingSheet ?? false + } + + public var isOpen: Bool { + + return isWindowLoaded && window!.isVisible + } +} diff --git a/Frameworks/RSCore/RSCore/Array+RSCore.swift b/Frameworks/RSCore/RSCore/Array+RSCore.swift new file mode 100644 index 000000000..ad8fc6da2 --- /dev/null +++ b/Frameworks/RSCore/RSCore/Array+RSCore.swift @@ -0,0 +1,20 @@ +// +// Array+RSCore.swift +// RSCore +// +// Created by Brent Simmons on 2/17/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public extension Array { + + public func firstElementPassingTest( _ test: (Element) -> Bool) -> Element? { + + guard let index = self.index(where: test) else { + return nil + } + return self[index] + } +} 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/CoalescingQueue.swift b/Frameworks/RSCore/RSCore/CoalescingQueue.swift new file mode 100644 index 000000000..c05bae751 --- /dev/null +++ b/Frameworks/RSCore/RSCore/CoalescingQueue.swift @@ -0,0 +1,96 @@ +// +// CoalescingQueue.swift +// RSCore +// +// Created by Brent Simmons on 2/17/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +// Use when you want to coalesce calls for something like updating visible table cells. +// Calls are uniqued. If you add a call with the same target and selector as a previous call, you’ll just get one call. +// Targets are weakly-held. If a target goes to nil, the call is not performed. +// The perform date is pushed off every time a call is added. +// Calls are FIFO. + +struct QueueCall: Equatable { + + weak var target: AnyObject? + let selector: Selector + + init(target: AnyObject, selector: Selector) { + + self.target = target + self.selector = selector + } + + func perform() { + + let _ = target?.perform(selector) + } + + static func ==(lhs: QueueCall, rhs: QueueCall) -> Bool { + + return lhs.target === rhs.target && lhs.selector == rhs.selector + } +} + +@objc public final class CoalescingQueue: NSObject { + + public static let standard = CoalescingQueue(name: "Standard") + public let name: String + private let interval: TimeInterval + private var timer: Timer? = nil + private var calls = [QueueCall]() + + public init(name: String, interval: TimeInterval = 0.05) { + + self.name = name + self.interval = interval + } + + public func add(_ target: AnyObject, _ selector: Selector) { + + let queueCall = QueueCall(target: target, selector: selector) + add(queueCall) + } + + @objc func timerDidFire(_ sender: Any?) { + + let callsToMake = calls // Make a copy in case calls are added to the queue while performing calls. + resetCalls() + callsToMake.forEach { $0.perform() } + } +} + +private extension CoalescingQueue { + + func add(_ call: QueueCall) { + + restartTimer() + + if !calls.contains(call) { + calls.append(call) + } + } + + func resetCalls() { + + calls = [QueueCall]() + } + + func restartTimer() { + + invalidateTimer() + timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(timerDidFire(_:)), userInfo: nil, repeats: false) + } + + func invalidateTimer() { + + if let timer = timer, timer.isValid { + timer.invalidate() + } + timer = nil + } +} diff --git a/Frameworks/RSCore/RSCore/DiskSaver.swift b/Frameworks/RSCore/RSCore/DiskSaver.swift deleted file mode 100755 index c129d12ae..000000000 --- a/Frameworks/RSCore/RSCore/DiskSaver.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// DiskSaver.swift -// RSCore -// -// Created by Brent Simmons on 12/28/15. -// Copyright © 2015 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public final class DiskSaver: NSObject { - - private let path: String - public weak var delegate: PlistProvider? - private var coalescedSaveTimer: Timer? - - public var dirty = false { - didSet { - if dirty { - coalescedSaveToDisk() - } - else { - invalidateSaveTimer() - } - } - } - - public init(path: String) { - - self.path = path - } - - deinit { - - if let timer = coalescedSaveTimer, timer.isValid { - timer.invalidate() - } - } - - private func invalidateSaveTimer() { - - if let timer = coalescedSaveTimer, timer.isValid { - timer.invalidate() - } - coalescedSaveTimer = nil - } - - private let coalescedSaveInterval = 1.0 - - private func coalescedSaveToDisk() { - - invalidateSaveTimer() - coalescedSaveTimer = Timer.scheduledTimer(timeInterval: coalescedSaveInterval, target: self, selector: #selector(saveToDisk), userInfo: nil, repeats: false) - } - - @objc public dynamic func saveToDisk() { - - invalidateSaveTimer() - if !dirty { - return - } - if let d = delegate?.plist { - - do { - try RSPlist.write(d, filePath: path) - dirty = false - } - catch { - print("DiskSaver: error writing \(path) to disk.") - } - } - } -} diff --git a/Frameworks/RSCore/RSCore/NSObject+RSCore.h b/Frameworks/RSCore/RSCore/NSObject+RSCore.h index afbe78c73..460560379 100755 --- a/Frameworks/RSCore/RSCore/NSObject+RSCore.h +++ b/Frameworks/RSCore/RSCore/NSObject+RSCore.h @@ -19,11 +19,6 @@ NS_ASSUME_NONNULL_BEGIN @interface NSObject (RSCore) -/*Cancels any previous and does a new -performSelector:withObject:afterDelay:. Experimental.*/ - -- (void)rs_performSelectorCoalesced:(SEL)selector withObject:(id _Nullable)obj afterDelay:(NSTimeInterval)delay - NS_SWIFT_NAME(performSelectorCoalesced(_:with:delay:)); - - (void)rs_takeValuesFromObject:(id)object propertyNames:(NSArray *)propertyNames; diff --git a/Frameworks/RSCore/RSCore/NSObject+RSCore.m b/Frameworks/RSCore/RSCore/NSObject+RSCore.m index 13156448c..3dd0ee409 100755 --- a/Frameworks/RSCore/RSCore/NSObject+RSCore.m +++ b/Frameworks/RSCore/RSCore/NSObject+RSCore.m @@ -50,13 +50,6 @@ BOOL RSEqualValues(id obj1, id obj2) { @implementation NSObject (RSCore) -- (void)rs_performSelectorCoalesced:(SEL)selector withObject:(id)obj afterDelay:(NSTimeInterval)delay { - - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:selector object:obj]; - [self performSelector:selector withObject:obj afterDelay:delay]; -} - - - (void)rs_takeValuesFromObject:(id)object propertyNames:(NSArray *)propertyNames { for (NSString *onePropertyName in propertyNames) { 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/RSCore/RSCore/NSWindow-Extensions.swift b/Frameworks/RSCore/RSCore/NSWindow-Extensions.swift index 0b878fc7f..69278e967 100755 --- a/Frameworks/RSCore/RSCore/NSWindow-Extensions.swift +++ b/Frameworks/RSCore/RSCore/NSWindow-Extensions.swift @@ -9,7 +9,12 @@ import AppKit public extension NSWindow { - + + public var isDisplayingSheet: Bool { + + return attachedSheet != nil + } + public func makeFirstResponderUnlessDescendantIsFirstResponder(_ responder: NSResponder) { if let fr = firstResponder, fr.hasAncestor(responder) { 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 } +} 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/RSParser/Feeds/JSON/JSONFeedParser.swift b/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift index b1546b0e8..166898af4 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 { @@ -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,35 @@ 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 lowerFeedURL = feedURL.lowercased() + let matchStrings = ["kottke.org", "pxlnv.com"] + for matchString in matchStrings { + if lowerFeedURL.contains(matchString) { + return true + } + } + + return false + } + static func parseUniqueID(_ itemDictionary: JSONDictionary) -> String? { if let uniqueID = itemDictionary[Key.uniqueID] as? String { 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 { 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 } } 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