Add a whole ton more code.

This commit is contained in:
Brent Simmons 2017-05-27 10:43:27 -07:00
parent 4969c44c40
commit 19ce82329b
60 changed files with 5299 additions and 64 deletions

View File

@ -17,6 +17,42 @@
8471A2C51ED4CEBF008F099E /* DataModel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8471A2B71ED4CEAD008F099E /* DataModel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 8471A2C51ED4CEBF008F099E /* DataModel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8471A2B71ED4CEAD008F099E /* DataModel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
8471A2F51ED4D062008F099E /* LocalAccount.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8471A2F21ED4D04D008F099E /* LocalAccount.framework */; }; 8471A2F51ED4D062008F099E /* LocalAccount.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8471A2F21ED4D04D008F099E /* LocalAccount.framework */; };
8471A2F61ED4D062008F099E /* LocalAccount.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8471A2F21ED4D04D008F099E /* LocalAccount.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 8471A2F61ED4D062008F099E /* LocalAccount.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8471A2F21ED4D04D008F099E /* LocalAccount.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97421ED9EAA9007D329B /* AddFolderWindowController.swift */; };
849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97511ED9EAC0007D329B /* AddFeedController.swift */; };
849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97521ED9EAC0007D329B /* AddFeedWindowController.swift */; };
849A975A1ED9EB0D007D329B /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97571ED9EB0D007D329B /* AccountManager.swift */; };
849A975B1ED9EB0D007D329B /* ArticleUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97581ED9EB0D007D329B /* ArticleUtilities.swift */; };
849A975C1ED9EB0D007D329B /* DefaultFeedsImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */; };
849A975E1ED9EB72007D329B /* MainWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A975D1ED9EB72007D329B /* MainWindowController.swift */; };
849A97641ED9EB96007D329B /* SidebarOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97601ED9EB96007D329B /* SidebarOutlineView.swift */; };
849A97651ED9EB96007D329B /* SidebarTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97611ED9EB96007D329B /* SidebarTreeControllerDelegate.swift */; };
849A97661ED9EB96007D329B /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97621ED9EB96007D329B /* SidebarViewController.swift */; };
849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97631ED9EB96007D329B /* UnreadCountView.swift */; };
849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */; };
849A976D1ED9EBC8007D329B /* TimelineTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A976A1ED9EBC8007D329B /* TimelineTableView.swift */; };
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A976B1ED9EBC8007D329B /* TimelineViewController.swift */; };
849A97761ED9EC04007D329B /* TimelineCellAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97701ED9EC04007D329B /* TimelineCellAppearance.swift */; };
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97711ED9EC04007D329B /* TimelineCellData.swift */; };
849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97721ED9EC04007D329B /* TimelineCellLayout.swift */; };
849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97731ED9EC04007D329B /* TimelineStringUtilities.swift */; };
849A977A1ED9EC04007D329B /* TimelineTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97741ED9EC04007D329B /* TimelineTableCellView.swift */; };
849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */; };
849A977F1ED9EC42007D329B /* ArticleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */; };
849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977E1ED9EC42007D329B /* DetailViewController.swift */; };
849A97831ED9EC63007D329B /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97821ED9EC63007D329B /* StatusBarView.swift */; };
849A97851ED9ECCD007D329B /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97841ED9ECCD007D329B /* PreferencesWindowController.swift */; };
849A97891ED9ECEF007D329B /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; };
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; };
849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A978C1ED9EE4D007D329B /* FeedListWindowController.swift */; };
849A978F1ED9EE72007D329B /* DefaultFeeds.plist in Resources */ = {isa = PBXBuildFile; fileRef = 849A978E1ED9EE72007D329B /* DefaultFeeds.plist */; };
849A97921ED9EF65007D329B /* IndeterminateProgressWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97911ED9EF65007D329B /* IndeterminateProgressWindowController.swift */; };
849A97951ED9EF7A007D329B /* IndeterminateProgressWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 849A97931ED9EF7A007D329B /* IndeterminateProgressWindow.xib */; };
849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; };
849A979C1ED9EFEB007D329B /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 849A979A1ED9EFEB007D329B /* styleSheet.css */; };
849A979D1ED9EFEB007D329B /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 849A979B1ED9EFEB007D329B /* template.html */; };
849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A979E1ED9F130007D329B /* SidebarCell.swift */; };
849A97A21ED9F180007D329B /* FeedTitleDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A01ED9F180007D329B /* FeedTitleDownloader.swift */; };
849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; };
849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849C64631ED37A5D003D8FC0 /* AppDelegate.swift */; }; 849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849C64631ED37A5D003D8FC0 /* AppDelegate.swift */; };
849C64661ED37A5D003D8FC0 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849C64651ED37A5D003D8FC0 /* ViewController.swift */; }; 849C64661ED37A5D003D8FC0 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849C64651ED37A5D003D8FC0 /* ViewController.swift */; };
849C64681ED37A5D003D8FC0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 849C64671ED37A5D003D8FC0 /* Assets.xcassets */; }; 849C64681ED37A5D003D8FC0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 849C64671ED37A5D003D8FC0 /* Assets.xcassets */; };
@ -313,12 +349,48 @@
842E45E61ED8C747000A8B52 /* DB5.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = DB5.plist; path = Evergreen/Resources/DB5.plist; sourceTree = "<group>"; }; 842E45E61ED8C747000A8B52 /* DB5.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = DB5.plist; path = Evergreen/Resources/DB5.plist; sourceTree = "<group>"; };
8471A2B21ED4CEAD008F099E /* DataModel.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = DataModel.xcodeproj; path = Frameworks/DataModel/DataModel.xcodeproj; sourceTree = "<group>"; }; 8471A2B21ED4CEAD008F099E /* DataModel.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = DataModel.xcodeproj; path = Frameworks/DataModel/DataModel.xcodeproj; sourceTree = "<group>"; };
8471A2EC1ED4D04D008F099E /* LocalAccount.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = LocalAccount.xcodeproj; path = Frameworks/LocalAccount/LocalAccount.xcodeproj; sourceTree = "<group>"; }; 8471A2EC1ED4D04D008F099E /* LocalAccount.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = LocalAccount.xcodeproj; path = Frameworks/LocalAccount/LocalAccount.xcodeproj; sourceTree = "<group>"; };
849A97421ED9EAA9007D329B /* AddFolderWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderWindowController.swift; sourceTree = "<group>"; };
849A97511ED9EAC0007D329B /* AddFeedController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AddFeedController.swift; path = AddFeed/AddFeedController.swift; sourceTree = "<group>"; };
849A97521ED9EAC0007D329B /* AddFeedWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AddFeedWindowController.swift; path = AddFeed/AddFeedWindowController.swift; sourceTree = "<group>"; };
849A97571ED9EB0D007D329B /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = "<group>"; };
849A97581ED9EB0D007D329B /* ArticleUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleUtilities.swift; sourceTree = "<group>"; };
849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultFeedsImporter.swift; sourceTree = "<group>"; };
849A975D1ED9EB72007D329B /* MainWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindowController.swift; sourceTree = "<group>"; };
849A97601ED9EB96007D329B /* SidebarOutlineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarOutlineView.swift; sourceTree = "<group>"; };
849A97611ED9EB96007D329B /* SidebarTreeControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarTreeControllerDelegate.swift; sourceTree = "<group>"; };
849A97621ED9EB96007D329B /* SidebarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = "<group>"; };
849A97631ED9EB96007D329B /* UnreadCountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCountView.swift; sourceTree = "<group>"; };
849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineTableRowView.swift; sourceTree = "<group>"; };
849A976A1ED9EBC8007D329B /* TimelineTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineTableView.swift; sourceTree = "<group>"; };
849A976B1ED9EBC8007D329B /* TimelineViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
849A97701ED9EC04007D329B /* TimelineCellAppearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineCellAppearance.swift; sourceTree = "<group>"; };
849A97711ED9EC04007D329B /* TimelineCellData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineCellData.swift; sourceTree = "<group>"; };
849A97721ED9EC04007D329B /* TimelineCellLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineCellLayout.swift; sourceTree = "<group>"; };
849A97731ED9EC04007D329B /* TimelineStringUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineStringUtilities.swift; sourceTree = "<group>"; };
849A97741ED9EC04007D329B /* TimelineTableCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineTableCellView.swift; sourceTree = "<group>"; };
849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadIndicatorView.swift; sourceTree = "<group>"; };
849A977D1ED9EC42007D329B /* ArticleRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleRenderer.swift; sourceTree = "<group>"; };
849A977E1ED9EC42007D329B /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
849A97821ED9EC63007D329B /* StatusBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarView.swift; sourceTree = "<group>"; };
849A97841ED9ECCD007D329B /* PreferencesWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesWindowController.swift; path = Evergreen/Preferences/PreferencesWindowController.swift; sourceTree = "<group>"; };
849A97871ED9ECEF007D329B /* ArticleStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleStyle.swift; sourceTree = "<group>"; };
849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleStylesManager.swift; sourceTree = "<group>"; };
849A978C1ED9EE4D007D329B /* FeedListWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedListWindowController.swift; sourceTree = "<group>"; };
849A978E1ED9EE72007D329B /* DefaultFeeds.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = DefaultFeeds.plist; path = Evergreen/Resources/DefaultFeeds.plist; sourceTree = SOURCE_ROOT; };
849A97911ED9EF65007D329B /* IndeterminateProgressWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressWindowController.swift; sourceTree = "<group>"; };
849A97941ED9EF7A007D329B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Evergreen/Base.lproj/IndeterminateProgressWindow.xib; sourceTree = SOURCE_ROOT; };
849A97971ED9EFAA007D329B /* Node-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Node-Extensions.swift"; sourceTree = "<group>"; };
849A979A1ED9EFEB007D329B /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = styleSheet.css; path = Evergreen/Resources/styleSheet.css; sourceTree = SOURCE_ROOT; };
849A979B1ED9EFEB007D329B /* template.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = template.html; path = Evergreen/Resources/template.html; sourceTree = SOURCE_ROOT; };
849A979E1ED9F130007D329B /* SidebarCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarCell.swift; sourceTree = "<group>"; };
849A97A01ED9F180007D329B /* FeedTitleDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeedTitleDownloader.swift; path = AddFeed/FeedTitleDownloader.swift; sourceTree = "<group>"; };
849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FolderTreeControllerDelegate.swift; path = AddFeed/FolderTreeControllerDelegate.swift; sourceTree = "<group>"; };
849C64601ED37A5D003D8FC0 /* Evergreen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Evergreen.app; sourceTree = BUILT_PRODUCTS_DIR; }; 849C64601ED37A5D003D8FC0 /* Evergreen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Evergreen.app; sourceTree = BUILT_PRODUCTS_DIR; };
849C64631ED37A5D003D8FC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Evergreen/AppDelegate.swift; sourceTree = "<group>"; }; 849C64631ED37A5D003D8FC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Evergreen/AppDelegate.swift; sourceTree = "<group>"; };
849C64651ED37A5D003D8FC0 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = Evergreen/ViewController.swift; sourceTree = "<group>"; }; 849C64651ED37A5D003D8FC0 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = Evergreen/ViewController.swift; sourceTree = "<group>"; };
849C64671ED37A5D003D8FC0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Evergreen/Assets.xcassets; sourceTree = "<group>"; }; 849C64671ED37A5D003D8FC0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Evergreen/Assets.xcassets; sourceTree = "<group>"; };
849C646A1ED37A5D003D8FC0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; 849C646A1ED37A5D003D8FC0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
849C646C1ED37A5D003D8FC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Evergreen/Info.plist; sourceTree = "<group>"; }; 849C646C1ED37A5D003D8FC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Info.plist; sourceTree = "<group>"; };
849C64711ED37A5D003D8FC0 /* EvergreenTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EvergreenTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 849C64751ED37A5D003D8FC0 /* EvergreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvergreenTests.swift; sourceTree = "<group>"; };
849C64771ED37A5D003D8FC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 849C64771ED37A5D003D8FC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -364,6 +436,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
842E45DE1ED8C582000A8B52 /* Defaults.swift */, 842E45DE1ED8C582000A8B52 /* Defaults.swift */,
849A97841ED9ECCD007D329B /* PreferencesWindowController.swift */,
); );
name = Preferences; name = Preferences;
sourceTree = "<group>"; sourceTree = "<group>";
@ -372,7 +445,14 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
842E45E21ED8C681000A8B52 /* KeyboardDelegateProtocol.swift */, 842E45E21ED8C681000A8B52 /* KeyboardDelegateProtocol.swift */,
849A975D1ED9EB72007D329B /* MainWindowController.swift */,
842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */, 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */,
849A975F1ED9EB95007D329B /* Sidebar */,
849A97681ED9EBC8007D329B /* Timeline */,
849A977C1ED9EC42007D329B /* Detail */,
849A97811ED9EC63007D329B /* Status Bar */,
849A97551ED9EAC3007D329B /* Add Feed */,
849A97411ED9EAA9007D329B /* Add Folder */,
); );
name = MainWindow; name = MainWindow;
path = Evergreen/MainWindow; path = Evergreen/MainWindow;
@ -395,6 +475,141 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
849A97411ED9EAA9007D329B /* Add Folder */ = {
isa = PBXGroup;
children = (
849A97421ED9EAA9007D329B /* AddFolderWindowController.swift */,
);
name = "Add Folder";
path = AddFolder;
sourceTree = "<group>";
};
849A97551ED9EAC3007D329B /* Add Feed */ = {
isa = PBXGroup;
children = (
849A97511ED9EAC0007D329B /* AddFeedController.swift */,
849A97521ED9EAC0007D329B /* AddFeedWindowController.swift */,
849A97A01ED9F180007D329B /* FeedTitleDownloader.swift */,
849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */,
);
name = "Add Feed";
sourceTree = "<group>";
};
849A97561ED9EB0D007D329B /* Data */ = {
isa = PBXGroup;
children = (
849A97571ED9EB0D007D329B /* AccountManager.swift */,
849A97581ED9EB0D007D329B /* ArticleUtilities.swift */,
849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */,
849A978E1ED9EE72007D329B /* DefaultFeeds.plist */,
);
name = Data;
path = Evergreen/Data;
sourceTree = "<group>";
};
849A975F1ED9EB95007D329B /* Sidebar */ = {
isa = PBXGroup;
children = (
849A97621ED9EB96007D329B /* SidebarViewController.swift */,
849A97601ED9EB96007D329B /* SidebarOutlineView.swift */,
849A979E1ED9F130007D329B /* SidebarCell.swift */,
849A97611ED9EB96007D329B /* SidebarTreeControllerDelegate.swift */,
849A97631ED9EB96007D329B /* UnreadCountView.swift */,
);
path = Sidebar;
sourceTree = "<group>";
};
849A97681ED9EBC8007D329B /* Timeline */ = {
isa = PBXGroup;
children = (
849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */,
849A976A1ED9EBC8007D329B /* TimelineTableView.swift */,
849A976B1ED9EBC8007D329B /* TimelineViewController.swift */,
849A976F1ED9EC04007D329B /* Cell */,
);
path = Timeline;
sourceTree = "<group>";
};
849A976F1ED9EC04007D329B /* Cell */ = {
isa = PBXGroup;
children = (
849A97701ED9EC04007D329B /* TimelineCellAppearance.swift */,
849A97711ED9EC04007D329B /* TimelineCellData.swift */,
849A97721ED9EC04007D329B /* TimelineCellLayout.swift */,
849A97731ED9EC04007D329B /* TimelineStringUtilities.swift */,
849A97741ED9EC04007D329B /* TimelineTableCellView.swift */,
849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */,
);
path = Cell;
sourceTree = "<group>";
};
849A977C1ED9EC42007D329B /* Detail */ = {
isa = PBXGroup;
children = (
849A977D1ED9EC42007D329B /* ArticleRenderer.swift */,
849A977E1ED9EC42007D329B /* DetailViewController.swift */,
);
path = Detail;
sourceTree = "<group>";
};
849A97811ED9EC63007D329B /* Status Bar */ = {
isa = PBXGroup;
children = (
849A97821ED9EC63007D329B /* StatusBarView.swift */,
);
name = "Status Bar";
path = StatusBar;
sourceTree = "<group>";
};
849A97861ED9ECEF007D329B /* Article Styles */ = {
isa = PBXGroup;
children = (
849A97871ED9ECEF007D329B /* ArticleStyle.swift */,
849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */,
);
name = "Article Styles";
path = Evergreen/ArticleStyles;
sourceTree = SOURCE_ROOT;
};
849A978B1ED9EE4D007D329B /* Feed List */ = {
isa = PBXGroup;
children = (
849A978C1ED9EE4D007D329B /* FeedListWindowController.swift */,
);
name = "Feed List";
path = Evergreen/FeedList;
sourceTree = "<group>";
};
849A97901ED9EF65007D329B /* Progress Window */ = {
isa = PBXGroup;
children = (
849A97931ED9EF7A007D329B /* IndeterminateProgressWindow.xib */,
849A97911ED9EF65007D329B /* IndeterminateProgressWindowController.swift */,
);
name = "Progress Window";
path = Evergreen/ProgressWindow;
sourceTree = "<group>";
};
849A97961ED9EFAA007D329B /* Extensions */ = {
isa = PBXGroup;
children = (
849A97971ED9EFAA007D329B /* Node-Extensions.swift */,
);
name = Extensions;
path = Evergreen/Extensions;
sourceTree = "<group>";
};
849A97991ED9EFB6007D329B /* Resources */ = {
isa = PBXGroup;
children = (
849A979A1ED9EFEB007D329B /* styleSheet.css */,
849A979B1ED9EFEB007D329B /* template.html */,
849C646C1ED37A5D003D8FC0 /* Info.plist */,
);
name = Resources;
path = Evergreen/Extensions;
sourceTree = "<group>";
};
849C64571ED37A5D003D8FC0 = { 849C64571ED37A5D003D8FC0 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -407,7 +622,12 @@
842E45DC1ED8C54B000A8B52 /* Browser.swift */, 842E45DC1ED8C54B000A8B52 /* Browser.swift */,
842E45E11ED8C681000A8B52 /* MainWindow */, 842E45E11ED8C681000A8B52 /* MainWindow */,
842E45E01ED8C587000A8B52 /* Preferences */, 842E45E01ED8C587000A8B52 /* Preferences */,
849C646C1ED37A5D003D8FC0 /* Info.plist */, 849A97861ED9ECEF007D329B /* Article Styles */,
849A978B1ED9EE4D007D329B /* Feed List */,
849A97901ED9EF65007D329B /* Progress Window */,
849A97561ED9EB0D007D329B /* Data */,
849A97961ED9EFAA007D329B /* Extensions */,
849A97991ED9EFB6007D329B /* Resources */,
849C64741ED37A5D003D8FC0 /* EvergreenTests */, 849C64741ED37A5D003D8FC0 /* EvergreenTests */,
849C64611ED37A5D003D8FC0 /* Products */, 849C64611ED37A5D003D8FC0 /* Products */,
84B06FC61ED37F7200F0B54B /* DB5.xcodeproj */, 84B06FC61ED37F7200F0B54B /* DB5.xcodeproj */,
@ -819,9 +1039,13 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
849A97951ED9EF7A007D329B /* IndeterminateProgressWindow.xib in Resources */,
849A978F1ED9EE72007D329B /* DefaultFeeds.plist in Resources */,
849A979D1ED9EFEB007D329B /* template.html in Resources */,
842E45E71ED8C747000A8B52 /* DB5.plist in Resources */, 842E45E71ED8C747000A8B52 /* DB5.plist in Resources */,
849C64681ED37A5D003D8FC0 /* Assets.xcassets in Resources */, 849C64681ED37A5D003D8FC0 /* Assets.xcassets in Resources */,
849C646B1ED37A5D003D8FC0 /* Main.storyboard in Resources */, 849C646B1ED37A5D003D8FC0 /* Main.storyboard in Resources */,
849A979C1ED9EFEB007D329B /* styleSheet.css in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -840,12 +1064,44 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
842E45DD1ED8C54B000A8B52 /* Browser.swift in Sources */, 842E45DD1ED8C54B000A8B52 /* Browser.swift in Sources */,
849A975B1ED9EB0D007D329B /* ArticleUtilities.swift in Sources */,
849A97891ED9ECEF007D329B /* ArticleStyle.swift in Sources */,
849A975A1ED9EB0D007D329B /* AccountManager.swift in Sources */,
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */,
849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */,
849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */,
849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */,
849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */,
849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */,
849A97831ED9EC63007D329B /* StatusBarView.swift in Sources */,
849C64661ED37A5D003D8FC0 /* ViewController.swift in Sources */, 849C64661ED37A5D003D8FC0 /* ViewController.swift in Sources */,
842E45DF1ED8C582000A8B52 /* Defaults.swift in Sources */, 842E45DF1ED8C582000A8B52 /* Defaults.swift in Sources */,
849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */,
849A97921ED9EF65007D329B /* IndeterminateProgressWindowController.swift in Sources */,
849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */,
849A975E1ED9EB72007D329B /* MainWindowController.swift in Sources */,
849A97661ED9EB96007D329B /* SidebarViewController.swift in Sources */,
842E45E31ED8C681000A8B52 /* KeyboardDelegateProtocol.swift in Sources */, 842E45E31ED8C681000A8B52 /* KeyboardDelegateProtocol.swift in Sources */,
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */,
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
842E45CE1ED8C308000A8B52 /* AppConstants.swift in Sources */, 842E45CE1ED8C308000A8B52 /* AppConstants.swift in Sources */,
849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */, 849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */,
849A975C1ED9EB0D007D329B /* DefaultFeedsImporter.swift in Sources */,
849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */,
849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */,
849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */,
842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */, 842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */,
849A976D1ED9EBC8007D329B /* TimelineTableView.swift in Sources */,
849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */,
849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */,
849A97851ED9ECCD007D329B /* PreferencesWindowController.swift in Sources */,
849A977A1ED9EC04007D329B /* TimelineTableCellView.swift in Sources */,
849A97641ED9EB96007D329B /* SidebarOutlineView.swift in Sources */,
849A97761ED9EC04007D329B /* TimelineCellAppearance.swift in Sources */,
849A97A21ED9F180007D329B /* FeedTitleDownloader.swift in Sources */,
849A97651ED9EB96007D329B /* SidebarTreeControllerDelegate.swift in Sources */,
849A977F1ED9EC42007D329B /* ArticleRenderer.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -918,6 +1174,14 @@
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
849A97931ED9EF7A007D329B /* IndeterminateProgressWindow.xib */ = {
isa = PBXVariantGroup;
children = (
849A97941ED9EF7A007D329B /* Base */,
);
name = IndeterminateProgressWindow.xib;
sourceTree = "<group>";
};
849C64691ED37A5D003D8FC0 /* Main.storyboard */ = { 849C64691ED37A5D003D8FC0 /* Main.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (

View File

@ -8,6 +8,8 @@
import Foundation import Foundation
let appName = "Evergreen"
extension Notification.Name { extension Notification.Name {
static let SidebarSelectionDidChange = Notification.Name("SidebarSelectionDidChangeNotification") static let SidebarSelectionDidChange = Notification.Name("SidebarSelectionDidChangeNotification")

View File

@ -2,25 +2,323 @@
// AppDelegate.swift // AppDelegate.swift
// Evergreen // Evergreen
// //
// Created by Brent Simmons on 5/22/17. // Created by Brent Simmons on 7/11/15.
// Copyright © 2017 Ranchero Software. All rights reserved. // Copyright © 2015 Ranchero Software, LLC. All rights reserved.
// //
import Cocoa import Cocoa
import DB5
import DataModel
import RSTextDrawing
import RSTree
import RSXML
var currentTheme: VSTheme!
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
let windowControllers = NSMutableArray()
var preferencesWindowController: NSWindowController?
var mainWindowController: NSWindowController?
var feedListWindowController: NSWindowController?
var addFeedController: AddFeedController?
var addFolderWindowController: AddFolderWindowController?
let themeLoader = VSThemeLoader()
private let appNewsURLString = "https://ranchero.com/evergreen/json.feed"
var unreadCount = 0 {
func applicationDidFinishLaunching(_ aNotification: Notification) { didSet {
// Insert code here to initialize your application updateBadgeCoalesced()
}
} }
func applicationWillTerminate(_ aNotification: Notification) { override init() {
// Insert code here to tear down your application
NSWindow.allowsAutomaticWindowTabbing = false
super.init()
}
// MARK: NSApplicationDelegate
func applicationDidFinishLaunching(_ note: Notification) {
registerDefaults()
currentTheme = themeLoader.defaultTheme
let _ = AccountManager.sharedInstance
let kFirstRunDateKey = "firstRun"
var isFirstRun = false
if UserDefaults.standard.object(forKey: kFirstRunDateKey) == nil {
isFirstRun = true
UserDefaults.standard.set(Date(), forKey: kFirstRunDateKey)
}
importDefaultFeedsIfNeeded(isFirstRun, account: AccountManager.sharedInstance.localAccount)
createAndShowMainWindow()
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
AccountManager.sharedInstance.updateUnreadCount()
#if RELEASE
DispatchQueue.main.async {
self.refreshAll(self)
}
#endif
NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(AppDelegate.getURL(_:_:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if (!flag) {
createAndShowMainWindow()
}
return false
}
func applicationDidResignActive(_ notification: Notification) {
RSSingleLineRenderer.emptyCache()
RSMultiLineRenderer.emptyCache()
TimelineCellData.emptyCache()
timelineEmptyCaches()
}
// MARK: GetURL Apple Event
func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor) {
guard let urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else {
return
}
let normalizedURLString = urlString.rs_normalizedURL()
if !normalizedURLString.rs_stringMayBeURL() {
return
}
DispatchQueue.main.async {
self.addFeed(normalizedURLString)
}
} }
// MARK: Badge
private func updateBadgeCoalesced() {
rs_performSelectorCoalesced(#selector(updateBadge), with: nil, afterDelay: 0.01)
}
dynamic func updateBadge() {
let label = unreadCount > 0 ? "\(unreadCount)" : ""
NSApplication.shared().dockTile.badgeLabel = label
}
// MARK: Notifications
func unreadCountDidChange(_ note: Notification) {
let updatedUnreadCount = AccountManager.sharedInstance.unreadCount
if updatedUnreadCount != unreadCount {
unreadCount = updatedUnreadCount
}
}
// MARK: Main Window
func windowControllerWithName(_ storyboardName: String) -> NSWindowController {
let storyboard = NSStoryboard(name: storyboardName, bundle: nil)
return storyboard.instantiateInitialController()! as! NSWindowController
}
func createAndShowMainWindow() {
if mainWindowController == nil {
mainWindowController = windowControllerWithName("MainWindow")
}
mainWindowController!.showWindow(self)
}
// MARK: NSUserInterfaceValidations
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if item.action == #selector(refreshAll(_:)) {
return !AccountManager.sharedInstance.refreshInProgress
}
if item.action == #selector(addAppNews(_:)) {
return !AccountManager.sharedInstance.anyAccountHasFeedWithURL(appNewsURLString)
}
return true
}
// MARK: Add Feed
func addFeed(_ urlString: String?, _ name: String? = nil) {
createAndShowMainWindow()
addFeedController = AddFeedController(hostWindow: mainWindowController!.window!)
addFeedController?.showAddFeedSheet(urlString, name)
}
// MARK: Actions
@IBAction func showPreferences(_ sender: AnyObject) {
if preferencesWindowController == nil {
preferencesWindowController = windowControllerWithName("Preferences")
}
preferencesWindowController!.showWindow(self)
}
@IBAction func showMainWindow(_ sender: AnyObject) {
createAndShowMainWindow()
}
@IBAction func refreshAll(_ sender: AnyObject) {
AccountManager.sharedInstance.refreshAll()
}
@IBAction func showAddFeedWindow(_ sender: AnyObject) {
addFeed(nil)
}
@IBAction func showAddFolderWindow(_ sender: AnyObject) {
createAndShowMainWindow()
addFolderWindowController = AddFolderWindowController()
addFolderWindowController!.runSheetOnWindow(mainWindowController!.window!)
}
@IBAction func showFeedList(_ sender: AnyObject) {
if feedListWindowController == nil {
feedListWindowController = windowControllerWithName("FeedList")
}
feedListWindowController!.showWindow(self)
}
// @IBAction func exportOPML(_ sender: AnyObject) {
//
// }
@IBAction func importOPMLFromFile(_ sender: AnyObject) {
let panel = NSOpenPanel()
panel.canDownloadUbiquitousContents = true
panel.canResolveUbiquitousConflicts = true
panel.canChooseFiles = true
panel.allowsMultipleSelection = false
panel.canChooseDirectories = false
panel.resolvesAliases = true
panel.allowedFileTypes = ["opml"]
panel.allowsOtherFileTypes = false
let result = panel.runModal()
if result == NSFileHandlingPanelOKButton {
if let url = panel.url {
DispatchQueue.main.async {
self.parseAndImportOPML(url, AccountManager.sharedInstance.localAccount)
}
}
}
}
@IBAction func exportOPML(_ sender: AnyObject) {
let panel = NSSavePanel()
panel.allowedFileTypes = ["opml"]
panel.allowsOtherFileTypes = false
panel.prompt = NSLocalizedString("Export OPML", comment: "Export OPML")
panel.title = NSLocalizedString("Export OPML", comment: "Export OPML")
panel.nameFieldLabel = NSLocalizedString("Export to:", comment: "Export OPML")
panel.message = NSLocalizedString("Choose a location for the exported OPML file.", comment: "Export OPML")
panel.isExtensionHidden = false
panel.nameFieldStringValue = "MySubscriptions.opml"
let result = panel.runModal()
if result == NSFileHandlingPanelOKButton {
if let url = panel.url {
DispatchQueue.main.async {
let opmlString = AccountManager.sharedInstance.localAccount.opmlString(indentLevel: 0)
do {
try opmlString.write(to: url, atomically: true, encoding: String.Encoding.utf8)
}
catch let error as NSError {
NSApplication.shared().presentError(error)
}
}
}
}
}
@IBAction func emailSupport(_ sender: AnyObject) {
let escapedAppName = appName.replacingOccurrences(of: " ", with: "%20")
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")!
let urlString = "mailto:support@ranchero.com?subject=I%20need%20help%20with%20\(escapedAppName)%20\(version)&body=I%20ran%20into%20a%20problem:%20"
if let url = URL(string: urlString) {
NSWorkspace.shared().open(url)
}
}
@IBAction func addAppNews(_ sender: AnyObject) {
if AccountManager.sharedInstance.anyAccountHasFeedWithURL(appNewsURLString) {
return
}
addFeed(appNewsURLString, "Evergreen News")
}
// @IBAction func importOPMLFromURL(_ sender: AnyObject) {
//
// }
}
private extension AppDelegate {
func parseAndImportOPML(_ url: URL, _ account: Account) {
var fileData: Data?
do {
fileData = try Data(contentsOf: url)
} catch {
print("Error reading OPML file. \(error)")
return
}
guard let opmlData = fileData else {
return
}
let xmlData = RSXMLData(data: opmlData, urlString: url.absoluteString)
RSParseOPML(xmlData) { (opmlDocument, error) in
if let error = error {
NSApplication.shared().presentError(error)
return
}
if let opmlDocument = opmlDocument {
account.importOPML(opmlDocument)
// account.refreshAll()
}
}
}
} }

View File

@ -0,0 +1,82 @@
//
// ArticleStyle.swift
// Evergreen
//
// Created by Brent Simmons on 9/26/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct ArticleStyle: Equatable {
static let defaultStyle = ArticleStyle()
let path: String?
let template: String?
let css: String?
let emptyCSS: String?
let info: NSDictionary?
init() {
//Default style
self.path = nil;
self.emptyCSS = nil
self.info = ["CreatorHomePage": "http://ranchero.com/", "CreatorName": "Ranchero Software, LLC", "Version": "1.0"]
let cssPath = Bundle.main.path(forResource: "styleSheet", ofType: "css")!
css = stringAtPath(cssPath)
let templatePath = Bundle.main.path(forResource: "template", ofType: "html")!
template = stringAtPath(templatePath)
}
init(path: String) {
self.path = path
let isFolder = FileManager.default.rs_fileIsFolder(path)
if isFolder {
let infoPath = (path as NSString).appendingPathComponent("Info.plist")
self.info = NSDictionary(contentsOfFile: infoPath)
let cssPath = (path as NSString).appendingPathComponent("stylesheet.css")
self.css = stringAtPath(cssPath)
let emptyCSSPath = (path as NSString).appendingPathComponent("stylesheet_empty.css")
self.emptyCSS = stringAtPath(emptyCSSPath)
let templatePath = (path as NSString).appendingPathComponent("template.html")
self.template = stringAtPath(templatePath)
}
else {
self.css = stringAtPath(path)
self.template = nil
self.emptyCSS = nil
self.info = nil
}
}
}
private func stringAtPath(_ f: String) -> String? {
if !FileManager.default.fileExists(atPath: f) {
return nil
}
if let s = try? NSString(contentsOfFile: f, usedEncoding: nil) as String {
return s
}
return nil
}
public func ==(lhs: ArticleStyle, rhs: ArticleStyle) -> Bool {
return lhs.path == rhs.path && lhs.template == rhs.template && lhs.css == rhs.css && lhs.info == rhs.info
}

View File

@ -0,0 +1,181 @@
//
// ArticleStylesManager.sqift
// Evergreen
//
// Created by Brent Simmons on 9/26/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
let ArticleStyleNamesDidChangeNotification = "ArticleStyleNamesDidChangeNotification"
let CurrentArticleStyleDidChangeNotification = "CurrentArticleStyleDidChangeNotification"
private let styleKey = "style"
private let defaultStyleName = "Default"
private let stylesFolderName = "Styles"
private let stylesInResourcesFolderName = "Styles"
private let styleSuffix = ".evergreenstyle"
private let nnwStyleSuffix = ".nnwstyle"
private let cssStyleSuffix = ".css"
private let styleSuffixes = [styleSuffix, nnwStyleSuffix, cssStyleSuffix];
public final class ArticleStylesManager {
static let sharedInstance = ArticleStylesManager()
private let folderPath = RSDataSubfolder(nil, stylesFolderName)!
var currentStyleName: String {
get {
return UserDefaults.standard.string(forKey: styleKey)!
}
set {
if newValue != currentStyleName {
UserDefaults.standard.set(newValue, forKey: styleKey)
}
}
}
var currentStyle: ArticleStyle {
didSet {
NotificationCenter.default.post(name: Notification.Name(rawValue: CurrentArticleStyleDidChangeNotification), object: self)
}
}
var styleNames = [defaultStyleName] {
didSet {
NotificationCenter.default.post(name: Notification.Name(rawValue: ArticleStyleNamesDidChangeNotification), object: self)
}
}
init() {
UserDefaults.standard.register(defaults: [styleKey: defaultStyleName])
let defaultStylesFolder = (Bundle.main.resourcePath! as NSString).appendingPathComponent(stylesInResourcesFolderName)
do {
try FileManager.default.rs_copyFiles(inFolder: defaultStylesFolder, destination: folderPath)
}
catch {
print(error)
}
currentStyle = ArticleStyle.defaultStyle
updateStyleNames()
updateCurrentStyle()
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: NSNotification.Name.NSApplicationDidBecomeActive, object: nil)
}
// MARK: Notifications
dynamic func applicationDidBecomeActive(_ note: Notification) {
updateStyleNames()
updateCurrentStyle()
}
// MARK : Internal
private func updateStyleNames() {
let updatedStyleNames = allStylePaths(folderPath).map { styleNameForPath($0) }
if updatedStyleNames != styleNames {
styleNames = updatedStyleNames
}
}
private func articleStyleWithStyleName(_ styleName: String) -> ArticleStyle? {
if styleName == defaultStyleName {
return ArticleStyle.defaultStyle
}
guard let path = pathForStyleName(styleName, folder: folderPath) else {
return nil
}
return ArticleStyle(path: path)
}
private func defaultArticleStyle() -> ArticleStyle {
return articleStyleWithStyleName(defaultStyleName)!
}
private func updateCurrentStyle() {
var styleName = currentStyleName
if !styleNames.contains(styleName) {
styleName = defaultStyleName
currentStyleName = defaultStyleName
}
var articleStyle = articleStyleWithStyleName(styleName)
if articleStyle == nil {
articleStyle = defaultArticleStyle()
currentStyleName = defaultStyleName
}
if let articleStyle = articleStyle, articleStyle != currentStyle {
currentStyle = articleStyle
}
}
}
private func allStylePaths(_ folder: String) -> [String] {
let filepaths = FileManager.default.rs_filepaths(inFolder: folder)
return filepaths.filter { fileAtPathIsStyle($0) }
}
private func fileAtPathIsStyle(_ f: String) -> Bool {
if !f.hasSuffix(styleSuffix) && !f.hasSuffix(nnwStyleSuffix) && !f.hasSuffix(cssStyleSuffix) {
return false
}
if (f as NSString).lastPathComponent.hasPrefix(".") {
return false
}
return true
}
private func filenameWithStyleSuffixRemoved(_ filename: String) -> String {
for oneSuffix in styleSuffixes {
if filename.hasSuffix(oneSuffix) {
return (filename as NSString).rs_string(byStrippingSuffix: oneSuffix, caseSensitive: false)
}
}
return filename
}
private func styleNameForPath(_ f: String) -> String {
let filename = (f as NSString).lastPathComponent
return filenameWithStyleSuffixRemoved(filename)
}
private func pathIsPathForStyleName(_ styleName: String, path: String) -> Bool {
let filename = (path as NSString).lastPathComponent
return filenameWithStyleSuffixRemoved(filename) == styleName
}
private func pathForStyleName(_ styleName: String, folder: String) -> String? {
for onePath in allStylePaths(folder) {
if pathIsPathForStyleName(styleName, path: onePath) {
return onePath
}
}
return nil
}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11198.2" systemVersion="15G1004" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11198.2"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="IndeterminateProgressWindowController" customModule="Rainier" customModuleProvider="target">
<connections>
<outlet property="messageLabel" destination="qcz-WI-q10" id="IJ9-M8-kzt"/>
<outlet property="progressIndicator" destination="MFX-Q2-XtZ" id="jpc-TD-TWd"/>
<outlet property="window" destination="NbJ-SP-fgw" id="e4d-eP-QkY"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="NbJ-SP-fgw" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" utility="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="272" y="172" width="267" height="85"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
<view key="contentView" id="gai-Qn-u7b">
<rect key="frame" x="0.0" y="0.0" width="267" height="85"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="qcz-WI-q10">
<rect key="frame" x="90" y="46" width="87" height="19"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Message…" id="84N-t0-TEo">
<font key="font" metaFont="systemBold" size="15"/>
<color key="textColor" white="0.96999999999999997" alpha="1" colorSpace="calibratedWhite"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="message" id="EL8-wI-e9T">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<bool key="NSRaisesForNotApplicableKeys" value="NO"/>
</dictionary>
</binding>
</connections>
</textField>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" bezeled="NO" indeterminate="YES" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="MFX-Q2-XtZ">
<rect key="frame" x="20" y="19" width="227" height="20"/>
</progressIndicator>
</subviews>
<constraints>
<constraint firstItem="qcz-WI-q10" firstAttribute="top" secondItem="gai-Qn-u7b" secondAttribute="top" constant="20" symbolic="YES" id="6hM-39-Lu6"/>
<constraint firstItem="qcz-WI-q10" firstAttribute="centerX" secondItem="gai-Qn-u7b" secondAttribute="centerX" id="B2C-49-Glg"/>
<constraint firstItem="MFX-Q2-XtZ" firstAttribute="centerX" secondItem="gai-Qn-u7b" secondAttribute="centerX" id="Byx-UU-xWP"/>
<constraint firstItem="MFX-Q2-XtZ" firstAttribute="leading" secondItem="gai-Qn-u7b" secondAttribute="leading" constant="20" symbolic="YES" id="J05-i6-91Y"/>
<constraint firstAttribute="bottom" secondItem="MFX-Q2-XtZ" secondAttribute="bottom" constant="20" symbolic="YES" id="ZRz-VV-P8J"/>
<constraint firstAttribute="trailing" secondItem="MFX-Q2-XtZ" secondAttribute="trailing" constant="20" symbolic="YES" id="k2g-Jl-o6g"/>
<constraint firstItem="MFX-Q2-XtZ" firstAttribute="top" secondItem="qcz-WI-q10" secondAttribute="bottom" constant="8" symbolic="YES" id="xxr-Ok-3Bb"/>
</constraints>
</view>
<point key="canvasLocation" x="-71.5" y="-167.5"/>
</window>
</objects>
</document>

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS"> <document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="12118" systemVersion="16F73" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11134"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="12118"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Application--> <!--Application-->
@ -21,7 +22,11 @@
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/> <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/> <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW">
<connections>
<action selector="showPreferences:" target="Ady-hI-5gd" id="Syu-AN-6P4"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/> <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz"> <menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
@ -653,41 +658,10 @@
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/> <outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections> </connections>
</application> </application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/> <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Evergreen" customModuleProvider="target"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="75" y="0.0"/> <point key="canvasLocation" x="75" y="0.0"/>
</scene> </scene>
<!--Window Controller-->
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
</window>
<connections>
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
</connections>
</windowController>
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="250"/>
</scene>
<!--View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" wantsLayer="YES" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="655"/>
</scene>
</scenes> </scenes>
</document> </document>

View File

@ -0,0 +1,222 @@
//
// AccountManager.swift
// Evergreen
//
// Created by Brent Simmons on 7/18/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import DataModel
import LocalAccount
let AccountsDidChangeNotification = "AccountsDidChangeNotification"
private let localAccountFolderName = "OnMyMac"
private let localAccountIdentifier = "OnMyMac"
final class AccountManager: UnreadCountProvider {
static let sharedInstance = AccountManager()
private let accountsFolder = RSDataSubfolder(nil, "Accounts")!
private var accountsDictionary = [String: Account]()
let localAccount: Account
var unreadCount = 0 {
didSet {
postUnreadCountDidChangeNotification()
}
}
var accounts: [Account] {
get {
return Array(accountsDictionary.values)
}
}
var sortedAccounts: [Account] {
get {
return accountsSortedByName()
}
}
var refreshInProgress: Bool {
get {
for oneAccount in accountsDictionary.values {
if oneAccount.refreshInProgress {
return true
}
}
return false
}
}
init() {
// The local "On My Mac" account must always exist, even if it's empty.
let localAccountFolder = (accountsFolder as NSString).appendingPathComponent("OnMyMac")
do {
try FileManager.default.createDirectory(atPath: localAccountFolder, withIntermediateDirectories: true, attributes: nil)
}
catch {
assertionFailure("Could not create folder for OnMyMac account.")
abort()
}
let localAccountSettingsFile = accountFilePathWithFolder(localAccountFolder)
localAccount = LocalAccount(settingsFile: localAccountSettingsFile, dataFolder: localAccountFolder, identifier: localAccountIdentifier)
accountsDictionary[localAccount.identifier] = localAccount
readNonLocalAccountsFromDisk()
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
}
// MARK: API
func existingAccountWithIdentifier(_ identifier: String) -> Account? {
return accountsDictionary[identifier]
}
func refreshAll() {
accounts.forEach { (oneAccount) in
oneAccount.refreshAll()
}
}
func anyAccountHasAtLeastOneFeed() -> Bool {
for oneAccount in accounts {
if oneAccount.hasAtLeastOneFeed {
return true
}
}
return false
}
func anyAccountHasFeedWithURL(_ urlString: String) -> Bool {
for oneAccount in accounts {
if let _ = oneAccount.existingFeedWithURL(urlString) {
return true
}
}
return false
}
// MARK: UnreadCountProvider
func updateUnreadCount() {
let updatedUnreadCount = calculateUnreadCount(accounts)
if updatedUnreadCount != unreadCount {
unreadCount = updatedUnreadCount
}
}
// MARK: Notifications
dynamic func unreadCountDidChange(_ notification: Notification) {
guard let _ = notification.object as? Account else {
return
}
updateUnreadCount()
}
// MARK: Private
private func createAccount(_ accountSpecifier: AccountSpecifier) -> Account? {
return nil
}
private func createAccount(_ filename: String) -> Account? {
let folderPath = (accountsFolder as NSString).appendingPathComponent(filename)
if let accountSpecifier = AccountSpecifier(folderPath: folderPath) {
return createAccount(accountSpecifier)
}
return nil
}
private func readNonLocalAccountsFromDisk() {
var filenames: [String]?
do {
filenames = try FileManager.default.contentsOfDirectory(atPath: accountsFolder)
}
catch {
print("Error reading Accounts folder: \(error)")
return
}
filenames?.forEach { (oneFilename) in
guard oneFilename != localAccountFolderName else {
return
}
if let oneAccount = createAccount(oneFilename) {
accountsDictionary[oneAccount.identifier] = oneAccount
}
}
}
private func accountsSortedByName() -> [Account] {
// LocalAccount is first.
return accounts.sorted { (account1, account2) -> Bool in
if account1 === localAccount {
return true
}
if account2 === localAccount {
return false
}
//TODO: Use localizedCaseInsensitiveCompare:
return account1.nameForDisplay < account2.nameForDisplay
}
}
}
private let accountDataFileName = "AccountData.plist"
private func accountFilePathWithFolder(_ folderPath: String) -> String {
return NSString(string: folderPath).appendingPathComponent(accountDataFileName)
}
private struct AccountSpecifier {
let type: String
let identifier: String
let folderPath: String
let folderName: String
let dataFilePath: String
init?(folderPath: String) {
self.folderPath = folderPath
self.folderName = NSString(string: folderPath).lastPathComponent
let nameComponents = self.folderName.components(separatedBy: "-")
let satisfyCompilerFolderName = self.folderName
assert(nameComponents.count == 2, "Cant determine account info from \(satisfyCompilerFolderName)")
if nameComponents.count != 2 {
return nil
}
self.type = nameComponents[0]
self.identifier = nameComponents[1]
self.dataFilePath = accountFilePathWithFolder(self.folderPath)
}
}

View File

@ -0,0 +1,61 @@
//
// ArticleUtilities.swift
// Evergreen
//
// Created by Brent Simmons on 7/25/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import DataModel
// These handle multiple accounts.
func markArticles(_ articles: NSSet, statusKey: ArticleStatusKey, flag: Bool) {
let d: [String: NSSet] = accountAndArticlesDictionary(articles)
d.keys.forEach { (oneAccountIdentifier) in
guard let oneAccountArticles = d[oneAccountIdentifier], let oneAccount = accountWithIdentifier(oneAccountIdentifier) else {
return
}
oneAccount.markArticles(oneAccountArticles, statusKey: statusKey, flag: flag)
}
}
private func accountAndArticlesDictionary(_ articles: NSSet) -> [String: NSSet] {
var d = [String: NSMutableSet]()
articles.forEach { (oneObject) in
guard let oneArticle = oneObject as? Article else {
return
}
guard let oneAccountIdentifier = oneArticle.account?.identifier else {
return
}
let oneArticleSet: NSMutableSet = d[oneAccountIdentifier] ?? NSMutableSet()
oneArticleSet.add(oneArticle)
d[oneAccountIdentifier] = oneArticleSet
}
return d
}
private func accountWithIdentifier(_ identifier: String) -> Account? {
return AccountManager.sharedInstance.existingAccountWithIdentifier(identifier)
}
func preferredLink(for article: Article) -> String? {
if let s = article.permalink {
return s
}
return article.link
}

View File

@ -0,0 +1,53 @@
//
// DefaultFeedsImporter.swift
// Evergreen
//
// Created by Brent Simmons on 8/13/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import DataModel
import LocalAccount
private func shouldImportDefaultFeeds(_ isFirstRun: Bool) -> Bool {
if !isFirstRun {
return false
}
for oneAccount in AccountManager.sharedInstance.accounts {
if oneAccount.hasAtLeastOneFeed {
return false
}
}
return true
}
private func defaultFeedsArray() -> NSArray {
let f = Bundle.main.path(forResource: "DefaultFeeds", ofType: "plist")!
return NSArray(contentsOfFile: f)!
}
private func importFeedsWithArray(_ defaultFeeds: NSArray, _ account: Account) {
for d in defaultFeeds {
guard let oneFeedDictionary = d as? NSDictionary else {
continue
}
let oneFeed = LocalFeed(account: account, diskDictionary: oneFeedDictionary)!
let _ = account.addItem(oneFeed)
}
}
func importDefaultFeedsIfNeeded(_ isFirstRun: Bool, account: Account) {
if !shouldImportDefaultFeeds(isFirstRun) {
return
}
importFeedsWithArray(defaultFeedsArray(), account)
}

View File

@ -0,0 +1,51 @@
//
// Node-Extensions.swift
// Local
//
// Created by Brent Simmons on 8/10/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSTree
import DataModel
extension Node {
class func nodesSortedAlphabetically(_ nodes: [Node]) -> [Node] {
return nodes.sorted { (node1, node2) -> Bool in
guard let obj1 = node1.representedObject as? DisplayNameProvider, let obj2 = node2.representedObject as? DisplayNameProvider else {
return false
}
let name1 = obj1.nameForDisplay
let name2 = obj2.nameForDisplay
return name1.localizedStandardCompare(name2) == .orderedAscending
}
}
class func nodesSortedAlphabeticallyWithFoldersAtEnd(_ nodes: [Node]) -> [Node] {
return nodes.sorted { (node1, node2) -> Bool in
if node1.canHaveChildNodes != node2.canHaveChildNodes {
if node1.canHaveChildNodes {
return false
}
return true
}
guard let obj1 = node1.representedObject as? DisplayNameProvider, let obj2 = node2.representedObject as? DisplayNameProvider else {
return false
}
let name1 = obj1.nameForDisplay
let name2 = obj2.nameForDisplay
return name1.localizedStandardCompare(name2) == .orderedAscending
}
}
}

View File

@ -0,0 +1,15 @@
//
// FeedListWindowController.swift
// Evergreen
//
// Created by Brent Simmons on 8/1/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
class FeedListWindowController : NSWindowController {
}

View File

@ -0,0 +1,238 @@
//
// AddFeedController.swift
// Evergreen
//
// Created by Brent Simmons on 8/28/16.
// Copyright © 2016 Ranchero Software. All rights reserved.
//
import Cocoa
import RSCore
import RSTree
import DataModel
import RSFeedFinder
// Run add-feed sheet.
// If it returns with URL and optional name,
// run FeedFinder plus modal progress window.
// If FeedFinder returns feed,
// add feed.
// Else,
// display error sheet.
let UserDidAddFeedNotification = Notification.Name("UserDidAddFeedNotification")
let UserDidAddFeedKey = "feed"
class AddFeedController: AddFeedWindowControllerDelegate, FeedFinderDelegate {
fileprivate let hostWindow: NSWindow
fileprivate var addFeedWindowController: AddFeedWindowController?
fileprivate var userEnteredURL: URL?
fileprivate var userEnteredFolder: Folder?
fileprivate var userEnteredTitle: String?
fileprivate var foundFeedURLString: String?
fileprivate var titleFromFeed: String?
fileprivate var feedFinder: FeedFinder?
fileprivate var isFindingFeed = false
fileprivate var bestFeedSpecifier: FeedSpecifier?
init(hostWindow: NSWindow) {
self.hostWindow = hostWindow
}
func showAddFeedSheet(_ urlString: String?, _ name: String?) {
let folderTreeControllerDelegate = FolderTreeControllerDelegate()
let rootNode = Node(representedObject: AccountManager.sharedInstance.localAccount, parent: nil)
rootNode.canHaveChildNodes = true
let folderTreeController = TreeController(delegate: folderTreeControllerDelegate, rootNode: rootNode)
addFeedWindowController = AddFeedWindowController(urlString: urlString ?? urlStringFromPasteboard, name: name, folderTreeController: folderTreeController, delegate: self)
addFeedWindowController!.runSheetOnWindow(hostWindow)
}
// MARK: AddFeedWindowControllerDelegate
func addFeedWindowController(_: AddFeedWindowController, userEnteredURL url: URL, userEnteredTitle title: String?, folder: Folder) {
closeAddFeedSheet(NSModalResponseOK)
assert(folder.account != nil, "Folder must have an account.")
let account = folder.account ?? AccountManager.sharedInstance.localAccount
if account.hasFeedWithURLString(url.absoluteString) {
showAlreadySubscribedError(url.absoluteString, folder)
return
}
userEnteredURL = url
userEnteredFolder = folder
userEnteredTitle = title
findFeed()
}
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController) {
closeAddFeedSheet(NSModalResponseCancel)
}
// MARK: FeedFinderDelegate
public func feedFinder(_ feedFinder: FeedFinder, didFindFeeds feedSpecifiers: Set<FeedSpecifier>) {
isFindingFeed = false
endShowingProgress()
if let error = feedFinder.initialDownloadError {
if feedFinder.initialDownloadStatusCode == 404 {
showNoFeedsErrorMessage()
}
else {
showInitialDownloadError(error)
}
return
}
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers) else {
showNoFeedsErrorMessage()
return
}
self.bestFeedSpecifier = bestFeedSpecifier
self.foundFeedURLString = bestFeedSpecifier.urlString
if let _ = userEnteredTitle {
addFeedIfPossible()
}
if let url = URL(string: bestFeedSpecifier.urlString) {
downloadTitleForFeed(url, { (title) in
self.titleFromFeed = title
self.addFeedIfPossible()
})
}
else {
// Shouldn't happen.
showNoFeedsErrorMessage()
}
}
}
private extension AddFeedController {
var urlStringFromPasteboard: String? {
get {
if let urlString = NSPasteboard.rs_urlString(from: NSPasteboard.general()) {
return urlString.rs_normalizedURL()
}
return nil
}
}
func closeAddFeedSheet(_ returnCode: NSModalResponse) {
if let sheetWindow = addFeedWindowController?.window {
hostWindow.endSheet(sheetWindow, returnCode: returnCode)
}
}
func addFeedIfPossible() {
// Add feed if not already subscribed-to.
guard let folder = userEnteredFolder else {
assertionFailure("Folder must not be nil here.")
return
}
guard let account = userEnteredFolder?.account else {
assertionFailure("Folder must have an account.")
return
}
guard let feedURLString = foundFeedURLString else {
assertionFailure("urlString must not be nil here.")
return
}
if account.hasFeedWithURLString(feedURLString) {
showAlreadySubscribedError(feedURLString, folder)
return
}
if let feed = folder.createFeedWithName(titleFromFeed, editedName: userEnteredTitle, urlString: feedURLString) {
print(feedURLString)
if folder.addItem(feed) {
NotificationCenter.default.post(name: UserDidAddFeedNotification, object: self, userInfo: [UserDidAddFeedKey: feed])
}
}
}
// MARK: Find Feeds
func findFeed() {
guard let url = userEnteredURL else {
assertionFailure("userEnteredURL must not be nil.")
return
}
isFindingFeed = true
feedFinder = FeedFinder(url: url, delegate: self)
beginShowingProgress()
}
// MARK: Errors
func showAlreadySubscribedError(_ urlString: String, _ folder: Folder) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Already subscribed", comment: "Feed finder")
alert.informativeText = NSLocalizedString("Cant add this feed because youve already subscribed to it.", comment: "Feed finder")
alert.beginSheetModal(for: hostWindow)
}
func showInitialDownloadError(_ error: Error) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Download Error", comment: "Feed finder")
let formatString = NSLocalizedString("Cant add this feed because of a download error: “%@”", comment: "Feed finder")
let errorText = NSString.localizedStringWithFormat(formatString as NSString, error.localizedDescription)
alert.informativeText = errorText as String
alert.beginSheetModal(for: hostWindow)
}
func showNoFeedsErrorMessage() {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Feed not found", comment: "Feed finder")
alert.informativeText = NSLocalizedString("Cant add a feed because no feed was found.", comment: "Feed finder")
alert.beginSheetModal(for: hostWindow)
}
// MARK: Progress
func beginShowingProgress() {
runIndeterminateProgressWithMessage(NSLocalizedString("Finding feed…", comment:"Feed finder"))
}
func endShowingProgress() {
stopIndeterminateProgress()
hostWindow.makeKeyAndOrderFront(self)
}
}

View File

@ -0,0 +1,175 @@
//
// AddFeedWindowController.swift
// Evergreen
//
// Created by Brent Simmons on 8/1/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
import RSCore
import RSTree
import DataModel
protocol AddFeedWindowControllerDelegate: class {
// userEnteredURL will have already been validated and normalized.
func addFeedWindowController(_: AddFeedWindowController, userEnteredURL: URL, userEnteredTitle: String?, folder: Folder)
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController)
}
class AddFeedWindowController : NSWindowController {
@IBOutlet var urlTextField: NSTextField!
@IBOutlet var nameTextField: NSTextField!
@IBOutlet var addButton: NSButton!
@IBOutlet var folderPopupButton: NSPopUpButton!
private var urlString: String?
private var initialName: String?
fileprivate weak var delegate: AddFeedWindowControllerDelegate?
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 hostWindow: NSWindow!
convenience init(urlString: String?, name: String?, folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) {
self.init(windowNibName: "AddFeedSheet")
self.urlString = urlString
self.initialName = name
self.delegate = delegate
self.folderTreeController = folderTreeController
}
func runSheetOnWindow(_ w: NSWindow) {
hostWindow = w
hostWindow.beginSheet(window!) { (returnCode: NSModalResponse) -> Void in
}
}
override func windowDidLoad() {
if let urlString = urlString {
urlTextField.stringValue = urlString
}
if let initialName = initialName, !initialName.isEmpty {
nameTextField.stringValue = initialName
}
folderPopupButton.menu = createFolderPopupMenu()
updateUI()
}
private func updateUI() {
var addButtonEnabled = false
let urlString = urlTextField.stringValue
if urlString.rs_stringMayBeURL() {
addButtonEnabled = true
}
addButton.isEnabled = addButtonEnabled
}
// MARK: Actions
@IBAction func cancel(_ sender: AnyObject) {
cancelSheet()
}
@IBAction func addFeed(_ sender: AnyObject) {
let urlString = urlTextField.stringValue
let normalizedURLString = (urlString as NSString).rs_normalizedURL()
if normalizedURLString.isEmpty {
cancelSheet()
return;
}
guard let url = URL(string: normalizedURLString) else {
cancelSheet()
return
}
delegate?.addFeedWindowController(self, userEnteredURL: url, userEnteredTitle: userEnteredTitle, folder: selectedFolder()!)
}
@IBAction func localShowFeedList(_ sender: AnyObject) {
NSApplication.shared().sendAction(NSSelectorFromString("showFeedList:"), to: nil, from: sender)
hostWindow.endSheet(window!, returnCode: NSModalResponseContinue)
}
// MARK: NSTextFieldDelegate
override func controlTextDidEndEditing(_ obj: Notification) {
updateUI()
}
override func controlTextDidChange(_ obj: Notification) {
updateUI()
}
}
private extension AddFeedWindowController {
func cancelSheet() {
delegate?.addFeedWindowControllerUserDidCancel(self)
}
func selectedFolder() -> Folder? {
return folderPopupButton.selectedItem?.representedObject as? Folder
}
func createFolderPopupMenu() -> NSMenu {
let menu = NSMenu(title: "Folders")
let menuItem = NSMenuItem(title: NSLocalizedString("Top Level", comment: "Add Feed Sheet"), action: nil, keyEquivalent: "")
menuItem.representedObject = folderTreeController.rootNode.representedObject
menu.addItem(menuItem)
if let childNodes = folderTreeController.rootNode.childNodes {
addFolderItemsToMenuWithNodes(menu: menu, nodes: childNodes, indentationLevel: 1)
}
return menu
}
func addFolderItemsToMenuWithNodes(menu: NSMenu, nodes: [Node], indentationLevel: Int) {
nodes.forEach { (oneNode) in
if let nameProvider = oneNode.representedObject as? DisplayNameProvider {
let menuItem = NSMenuItem(title: nameProvider.nameForDisplay, action: nil, keyEquivalent: "")
menuItem.indentationLevel = indentationLevel
menuItem.representedObject = oneNode.representedObject
menu.addItem(menuItem)
if oneNode.numberOfChildNodes > 0 {
addFolderItemsToMenuWithNodes(menu: menu, nodes: oneNode.childNodes!, indentationLevel: indentationLevel + 1)
}
}
}
}
}

View File

@ -0,0 +1,28 @@
//
// FeedTitleDownloader.swift
// Evergreen
//
// Created by Brent Simmons on 9/3/16.
// Copyright © 2016 Ranchero Software. All rights reserved.
//
import Foundation
import RSXML
import RSWeb
func downloadTitleForFeed(_ url: URL, _ completionHandler: @escaping (_ title: String?) -> ()) {
download(url) { (data, response, error) in
guard let data = data else {
completionHandler(nil)
return
}
let xmlData = RSXMLData(data: data, urlString: url.absoluteString)
RSParseFeed(xmlData) { (parsedFeed : RSParsedFeed?, error: Error?) in
completionHandler(parsedFeed?.title)
}
}
}

View File

@ -0,0 +1,48 @@
//
// FolderTreeControllerDelegate.swift
// Evergreen
//
// Created by Brent Simmons on 8/10/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSTree
import DataModel
final class FolderTreeControllerDelegate: TreeControllerDelegate {
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
return node.isRoot ? childNodesForRootNode(node) : nil
}
}
private extension FolderTreeControllerDelegate {
func childNodesForRootNode(_ node: Node) -> [Node]? {
// Root node is Top Level and children are folders. Folders cant have subfolders.
// This will have to be revised later.
var folderNodes = [Node]()
let _ = AccountManager.sharedInstance.localAccount.visitChildren { (oneRepresentedObject) in
if let folder = oneRepresentedObject as? Folder {
folderNodes += [createNode(folder, parent: node)]
}
return false
}
return Node.nodesSortedAlphabetically(folderNodes)
}
func createNode(_ folder: Folder, parent: Node) -> Node {
let node = Node(representedObject: folder as AnyObject, parent: parent)
node.canHaveChildNodes = false
return node
}
}

View File

@ -0,0 +1,85 @@
//
// AddFolderWindowController.swift
// Evergreen
//
// Created by Brent Simmons on 8/1/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
import DataModel
//func addFolderWindowController() -> AddFolderWindowController {
//
// return AddFolderWindowController(windowNibName: "AddFolderSheet")
//}
class AddFolderWindowController : NSWindowController {
@IBOutlet var folderNameTextField: NSTextField!
@IBOutlet var accountPopupButton: NSPopUpButton!
var hostWindow: NSWindow?
convenience init() {
self.init(windowNibName: "AddFolderSheet")
}
// MARK: API
func runSheetOnWindow(_ w: NSWindow) {
hostWindow = w
hostWindow!.beginSheet(window!) { (returnCode: NSModalResponse) -> Void in
if returnCode == NSModalResponseOK {
self.addFolderIfNeeded()
}
}
}
// MARK: NSViewController
override func windowDidLoad() {
accountPopupButton.removeAllItems()
let menu = NSMenu()
for oneAccount in AccountManager.sharedInstance.sortedAccounts {
let oneMenuItem = NSMenuItem()
oneMenuItem.title = oneAccount.nameForDisplay
oneMenuItem.representedObject = oneAccount
menu.addItem(oneMenuItem)
}
accountPopupButton.menu = menu
}
// MARK: Private
private func addFolderIfNeeded() {
guard let menuItem = accountPopupButton.selectedItem else {
return
}
let account = menuItem.representedObject as! Account
let folderName = self.folderNameTextField.stringValue
if folderName.isEmpty {
return
}
let _ = account.ensureFolderWithName(folderName)
}
// MARK: Actions
@IBAction func cancel(_ sender: AnyObject) {
hostWindow!.endSheet(window!, returnCode: NSModalResponseCancel)
}
@IBAction func addFolder(_ sender: AnyObject) {
hostWindow!.endSheet(window!, returnCode: NSModalResponseOK)
}
}

View File

@ -0,0 +1,191 @@
//
// ArticleRenderer.swift
// Evergreen
//
// Created by Brent Simmons on 9/8/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import DataModel
var cachedStyleString = ""
var cachedTemplate = ""
class ArticleRenderer {
let article: Article
let articleStyle: ArticleStyle
lazy var longDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .medium
return dateFormatter
}()
lazy var mediumDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
return dateFormatter
}()
lazy var shortDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
return dateFormatter
}()
lazy var title: String = {
if let articleTitle = self.article.title {
return articleTitle
}
return ""
}()
lazy var baseURL: URL? = {
var s = self.article.permalink
if s == nil {
s = self.article.feed?.homePageURL
}
if s == nil {
s = self.article.feed?.url
}
if s == nil {
return nil
}
if let url = URL(string: s!) {
if url.scheme == "http" || url.scheme == "https" {
return url
}
}
return nil
}()
var html: String {
return renderedHTML()
}
init(article: Article, style: ArticleStyle) {
self.article = article
self.articleStyle = style
}
// MARK: Private
private func textInsideTag(_ text: String, _ tag: String) -> String {
return "<\(tag)>\(text)</\(tag)>"
}
private func styleString() -> String {
if let s = articleStyle.css {
return s
}
if cachedStyleString.isEmpty {
let path = Bundle.main.path(forResource: "styleSheet", ofType: "css")!
let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
cachedStyleString = "\n\(s)\n"
}
return cachedStyleString
}
private func template() -> String {
if let s = articleStyle.template {
return s
}
if cachedTemplate.isEmpty {
let path = Bundle.main.path(forResource: "template", ofType: "html")!
let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
cachedTemplate = s as String
}
return cachedTemplate
}
private func linkWithTextAndClass(_ text: String, _ href: String, _ className: String) -> String {
return "<a class=\"\(className)\" href=\"\(href)\">\(text)</a>"
}
private func linkWithText(_ text: String, _ href: String) -> String {
return "<a href=\"\(href)\">\(text)</a>"
}
private func titleOrTitleLink() -> String {
let link = preferredLink(for: article)
if let link = link {
return linkWithText(title, link)
}
return title
}
private func substitutions() -> [String: String] {
var d = [String: String]()
let title = titleOrTitleLink()
d["newsitem_title"] = title
d["article_title"] = title
let body = article.body == nil ? "" : article.body
d["article_description"] = body
d["newsitem_description"] = body
var feedLink = ""
if let feedTitle = article.feed?.nameForDisplay {
feedLink = feedTitle
if let feedURL = article.feed?.homePageURL {
feedLink = linkWithTextAndClass(feedTitle, feedURL, "feedLink")
}
}
d["feedlink"] = feedLink
d["feedlink_withfavicon"] = feedLink
let longDate = longDateFormatter.string(from: article.logicalDatePublished)
d["date_long"] = longDate
let mediumDate = mediumDateFormatter.string(from: article.logicalDatePublished)
d["date_medium"] = mediumDate
let shortDate = shortDateFormatter.string(from: article.logicalDatePublished)
d["date_short"] = shortDate
return d
}
private func renderedHTML() -> String {
var s = "<!DOCTYPE html><html><head>"
s += textInsideTag(title, "title")
s += textInsideTag(styleString(), "style")
s += "</head></body>"
s += RSMacroProcessor.renderedText(withTemplate: template(), substitutions: substitutions(), macroStart: "[[", macroEnd: "]]")
s += "</body></html>"
// print(s)
return s
}
}

View File

@ -0,0 +1,114 @@
//
// DetailViewController.swift
// Evergreen
//
// Created by Brent Simmons on 7/26/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import WebKit
import RSCore
import DataModel
class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDelegate {
var webview: WKWebView!
var article: Article? {
didSet {
reloadHTML()
}
}
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(timelineSelectionDidChange(_:)), name: .TimelineSelectionDidChange, object: nil)
let preferences = WKPreferences()
preferences.minimumFontSize = 12.0
preferences.javaScriptCanOpenWindowsAutomatically = false
preferences.javaEnabled = false
preferences.javaScriptEnabled = true
preferences.plugInsEnabled = false
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
webview = WKWebView(frame: self.view.bounds, configuration: configuration)
webview.uiDelegate = self
webview.navigationDelegate = self
webview.translatesAutoresizingMaskIntoConstraints = false
let boxView = self.view as! DetailBox
boxView.contentView = webview
boxView.rs_addFullSizeConstraints(forSubview: webview)
boxView.viewController = self
}
// MARK: Notifications
func timelineSelectionDidChange(_ note: Notification) {
let timelineView = note.userInfo?[viewKey] as! NSView
if timelineView.window! === self.view.window {
article = note.userInfo?[articleKey] as? Article
}
}
func viewWillStartLiveResize() {
webview.evaluateJavaScript("document.body.style.overflow = 'hidden';", completionHandler: nil)
}
func viewDidEndLiveResize() {
webview.evaluateJavaScript("document.body.style.overflow = 'visible';", completionHandler: nil)
}
// MARK: Private
private func reloadHTML() {
if let article = article {
let articleRenderer = ArticleRenderer(article: article, style: ArticleStylesManager.sharedInstance.currentStyle)
webview.loadHTMLString(articleRenderer.html, baseURL: articleRenderer.baseURL)
}
else {
webview.loadHTMLString("", baseURL: nil)
}
}
// MARK: WKNavigationDelegate
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
if let url = navigationAction.request.url {
openInBrowser(url.absoluteString)
}
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
}
class DetailBox: NSBox {
weak var viewController: DetailViewController?
override func viewWillStartLiveResize() {
viewController?.viewWillStartLiveResize()
}
override func viewDidEndLiveResize() {
viewController?.viewDidEndLiveResize()
}
}

View File

@ -0,0 +1,197 @@
//
// MainWindowController.swift
// Evergreen
//
// Created by Brent Simmons on 8/1/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
import DataModel
private let kWindowFrameKey = "MainWindow"
class MainWindowController : NSWindowController, NSUserInterfaceValidations {
// MARK: NSWindowController
override func windowDidLoad() {
super.windowDidLoad()
// window?.titleVisibility = .hidden
window?.setFrameUsingName(kWindowFrameKey, force: true)
detailSplitViewItem?.minimumThickness = 384
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillTerminate(_:)), name: .NSApplicationWillTerminate, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appNavigationKeyPressed(_:)), name: .AppNavigationKeyPressed, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
}
// MARK: Notifications
func applicationWillTerminate(_ note: Notification) {
window?.saveFrame(usingName: kWindowFrameKey)
}
func appNavigationKeyPressed(_ note: Notification) {
guard let key = note.userInfo?[appNavigationKey] as? Int else {
return
}
guard let contentView = window?.contentView, let view = note.object as? NSView, view.isDescendant(of: contentView) else {
return
}
print(key)
}
func refreshProgressDidChange(_ note: Notification) {
rs_performSelectorCoalesced(#selector(MainWindowController.coalescedMakeToolbarValidate(_:)), with: nil, afterDelay: 0.1)
}
// MARK: Toolbar
func coalescedMakeToolbarValidate(_ sender: Any) {
window?.toolbar?.validateVisibleItems()
}
// MARK: NSUserInterfaceValidations
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if item.action == #selector(openArticleInBrowser(_:)) {
return currentLink != nil
}
if item.action == #selector(nextUnread(_:)) {
return canGoToNextUnread()
}
if item.action == #selector(markAllAsRead(_:)) {
return canMarkAllAsRead()
}
return true
}
// MARK: Actions
@IBAction func openArticleInBrowser(_ sender: AnyObject?) {
if let link = currentLink {
openInBrowser(link)
}
}
@IBAction func nextUnread(_ sender: AnyObject?) {
guard let timelineViewController = timelineViewController, let sidebarViewController = sidebarViewController else {
return
}
func makeTimelineViewFirstResponder() {
window!.makeFirstResponderUnlessDescendantIsFirstResponder(timelineViewController.tableView)
}
if timelineViewController.canGoToNextUnread() {
timelineViewController.goToNextUnread()
makeTimelineViewFirstResponder()
}
else if sidebarViewController.canGoToNextUnread() {
sidebarViewController.goToNextUnread()
makeTimelineViewFirstResponder()
}
}
@IBAction func markAllAsRead(_ sender: AnyObject?) {
timelineViewController?.markAllAsRead()
}
@IBAction func toggleSidebar(_ sender: AnyObject?) {
splitViewController!.toggleSidebar(sender)
}
}
private extension MainWindowController {
var splitViewController: NSSplitViewController? {
get {
guard let viewController = contentViewController else {
return nil
}
return viewController.childViewControllers.first as? NSSplitViewController
}
}
var sidebarViewController: SidebarViewController? {
get {
return splitViewController?.splitViewItems[0].viewController as? SidebarViewController
}
}
var timelineViewController: TimelineViewController? {
get {
return splitViewController?.splitViewItems[1].viewController as? TimelineViewController
}
}
var detailSplitViewItem: NSSplitViewItem? {
return splitViewController?.splitViewItems[2]
}
var detailViewController: DetailViewController? {
get {
return splitViewController?.splitViewItems[2].viewController as? DetailViewController
}
}
var selectedArticles: [Article]? {
get {
return timelineViewController?.selectedArticles
}
}
var oneSelectedArticle: Article? {
get {
if let articles = selectedArticles {
return articles.count == 1 ? articles[0] : nil
}
return nil
}
}
var currentLink: String? {
get {
if let article = oneSelectedArticle {
return preferredLink(for: article)
}
return nil
}
}
func canGoToNextUnread() -> Bool {
guard let timelineViewController = timelineViewController, let sidebarViewController = sidebarViewController else {
return false
}
return timelineViewController.canGoToNextUnread() || sidebarViewController.canGoToNextUnread()
}
func canMarkAllAsRead() -> Bool {
return timelineViewController?.canMarkAllAsRead() ?? false
}
}

View File

@ -1,6 +1,6 @@
// //
// MainWindowSplitView.swift // MainWindowSplitView.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 2/5/16. // Created by Brent Simmons on 2/5/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -0,0 +1,125 @@
//
// SidebarCell.swift
// Evergreen
//
// Created by Brent Simmons on 8/1/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import DB5
private var textSizeCache = [String: NSSize]()
class SidebarCell : NSTableCellView {
var image: NSImage?
private let unreadCountView = UnreadCountView(frame: NSZeroRect)
var unreadCount: Int {
get {
return unreadCountView.unreadCount
}
set {
if unreadCountView.unreadCount != newValue {
unreadCountView.unreadCount = newValue
}
}
}
var name: String {
get {
if let s = textField?.stringValue {
return s
}
return ""
}
set {
if textField?.stringValue != newValue {
textField?.stringValue = newValue
needsDisplay = true
needsLayout = true
}
}
}
override var isFlipped: Bool {
get {
return true
}
}
private func commonInit() {
unreadCountView.translatesAutoresizingMaskIntoConstraints = false
addSubview(unreadCountView)
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func layout() {
resizeSubviews(withOldSize: NSZeroSize)
}
private let kTextFieldOriginX: CGFloat = 4.0
private let kTextFieldMarginRight: CGFloat = 4.0
private let kUnreadCountMarginLeft: CGFloat = 4.0
private let kUnreadCountMarginRight: CGFloat = 4.0
override func resizeSubviews(withOldSize oldSize: NSSize) {
var r = textField!.frame
r.origin.x = kTextFieldOriginX
r.size.width = NSWidth(bounds) - (kTextFieldOriginX + kTextFieldMarginRight);
let unreadCountSize = unreadCountView.intrinsicContentSize
if unreadCountSize.width > 0.1 {
r.size.width = NSWidth(bounds) - (kTextFieldOriginX + kUnreadCountMarginLeft + unreadCountSize.width + kUnreadCountMarginRight)
}
let size = textField!.intrinsicContentSize
r.size.height = size.height
r = rs_rectCenteredVertically(r)
r.origin.y -= 1.0
textField?.rs_setFrameIfNotEqual(r)
layoutUnreadCountView(unreadCountSize)
}
private func layoutUnreadCountView(_ size: NSSize) {
if size == NSZeroSize {
if !unreadCountView.isHidden {
unreadCountView.isHidden = true
}
return
}
if unreadCountView.isHidden {
unreadCountView.isHidden = false
}
var r = NSZeroRect
r.size = size
r.origin.x = NSMaxX(textField!.frame) + kUnreadCountMarginLeft
r = rs_rectCenteredVertically(r)
unreadCountView.rs_setFrameIfNotEqual(r)
}
}

View File

@ -0,0 +1,68 @@
//
// SidebarOutlineView.swift
// Evergreen
//
// Created by Brent Simmons on 11/17/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import AppKit
class SidebarOutlineView : NSOutlineView {
//MARK: NSResponder
override func keyDown(with event: NSEvent) {
guard !event.rs_keyIsModified() else {
super.keyDown(with: event)
return
}
let ch = Int(event.rs_unmodifiedCharacter())
if ch == NSNotFound {
super.keyDown(with: event)
return
}
var keyHandled = false
switch(ch) {
case NSRightArrowFunctionKey:
keyHandled = true
case NSDeleteFunctionKey:
keyHandled = true
Swift.print("NSDeleteFunctionKey")
default:
keyHandled = false
}
if keyHandled {
NotificationCenter.default.post(name: .AppNavigationKeyPressed, object: self, userInfo: [appNavigationKey: ch])
}
else {
super.keyDown(with: event)
}
}
override func viewWillStartLiveResize() {
if let scrollView = self.enclosingScrollView {
scrollView.hasVerticalScroller = false
}
super.viewWillStartLiveResize()
}
override func viewDidEndLiveResize() {
if let scrollView = self.enclosingScrollView {
scrollView.hasVerticalScroller = true
}
super.viewDidEndLiveResize()
}
}

View File

@ -0,0 +1,115 @@
//
// SidebarTreeControllerDelegate.swift
// Evergreen
//
// Created by Brent Simmons on 7/24/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSTree
import DataModel
final class SidebarTreeControllerDelegate: TreeControllerDelegate {
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
if node.isRoot {
return childNodesForRootNode(node)
}
if node.representedObject is Folder {
return childNodesForFolderNode(node)
}
return nil
}
}
private extension SidebarTreeControllerDelegate {
func childNodesForRootNode(_ node: Node) -> [Node]? {
// The child nodes are the top-level items of the local Account.
// This will be expanded later to add synthetic feeds (All Unread, for instance).
var updatedChildNodes = [Node]()
let _ = AccountManager.sharedInstance.localAccount.visitChildren { (oneRepresentedObject) in
if let existingNode = node.childNodeRepresentingObject(oneRepresentedObject as AnyObject) {
// Reuse nodes.
if !updatedChildNodes.contains(existingNode) {
updatedChildNodes += [existingNode]
return false
}
}
if let newNode = createNode(representedObject: oneRepresentedObject as AnyObject, parent: node) {
updatedChildNodes += [newNode]
}
return false
}
updatedChildNodes = Node.nodesSortedAlphabeticallyWithFoldersAtEnd(updatedChildNodes)
return updatedChildNodes
}
func childNodesForFolderNode(_ node: Node) -> [Node]? {
var updatedChildNodes = [Node]()
let folder = node.representedObject as! Folder
let _ = folder.visitChildren { (oneRepresentedObject) -> Bool in
if let existingNode = node.childNodeRepresentingObject(oneRepresentedObject) {
if !updatedChildNodes.contains(existingNode) {
updatedChildNodes += [existingNode]
}
return false
}
if let newNode = self.createNode(representedObject: oneRepresentedObject, parent: node) {
updatedChildNodes += [newNode]
}
return false
}
updatedChildNodes = Node.nodesSortedAlphabeticallyWithFoldersAtEnd(updatedChildNodes)
return updatedChildNodes
}
func createNode(representedObject: AnyObject, parent: Node) -> Node? {
if let feed = representedObject as? Feed {
return createNode(feed: feed, parent: parent)
}
if let folder = representedObject as? Folder {
return createNode(folder: folder, parent: parent)
}
return nil
}
func createNode(feed: Feed, parent: Node) -> Node {
return Node(representedObject: feed, parent: parent)
}
func createNode(folder: Folder, parent: Node) -> Node {
let node = Node(representedObject: folder as AnyObject, parent: parent)
node.canHaveChildNodes = true
return node
}
func nodeInArrayRepresentingObject(_ nodes: [Node], _ representedObject: AnyObject) -> Node? {
for oneNode in nodes {
if oneNode.representedObject === representedObject {
return oneNode
}
}
return nil
}
}

View File

@ -0,0 +1,353 @@
//
// SidebarViewController.swift
// Evergreen
//
// Created by Brent Simmons on 7/26/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
import RSTree
import DataModel
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource {
@IBOutlet var outlineView: NSOutlineView!
var treeController: TreeController!
let treeControllerDelegate = SidebarTreeControllerDelegate()
//MARK: NSViewController
override func viewDidLoad() {
treeController = TreeController(delegate: treeControllerDelegate)
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(folderChildrenDidChange(_:)), name: NSNotification.Name(rawValue: FolderChildrenDidChangeNotification), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: UserDidAddFeedNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(dataModelDidPerformBatchUpdates(_:)), name: .DataModelDidPerformBatchUpdates, object: nil)
outlineView.reloadData()
}
//MARK: Notifications
dynamic func unreadCountDidChange(_ note: Notification) {
guard let representedObject = note.object else {
return
}
let _ = configureCellsForRepresentedObject(representedObject as AnyObject)
}
dynamic func folderChildrenDidChange(_ note: Notification) {
rebuildTreeAndReloadDataIfNeeded()
}
dynamic func dataModelDidPerformBatchUpdates(_ notification: Notification) {
rebuildTreeAndReloadDataIfNeeded()
}
dynamic func userDidAddFeed(_ note: Notification) {
// Find the feed and select it.
guard let feed = note.userInfo?[UserDidAddFeedKey] as? Feed else {
return
}
revealAndSelectRepresentedObject(feed)
}
// MARK: Actions
@IBAction func delete(_ sender: AnyObject?) {
if outlineView.selectionIsEmpty {
return
}
let nodesToDelete = selectedNodes
let selectedRows = outlineView.selectedRowIndexes
outlineView.beginUpdates()
outlineView.removeItems(at: selectedRows, inParent: nil, withAnimation: [.slideDown])
outlineView.endUpdates()
performDataModelBatchUpdates {
deleteItemsForNodes(nodesToDelete)
}
treeController.rebuild()
}
// MARK: Navigation
func canGoToNextUnread() -> Bool {
if let _ = rowContainingNextUnread() {
return true
}
return false
}
func goToNextUnread() {
guard let row = rowContainingNextUnread() else {
assertionFailure("goToNextUnread called before checking if there is a next unread.")
return
}
outlineView.selectRowIndexes(IndexSet([row]), byExtendingSelection: false)
NSApplication.shared().sendAction(NSSelectorFromString("nextUnread:"), to: nil, from: self)
}
// MARK: NSOutlineViewDelegate
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
let cell = outlineView.make(withIdentifier: "DataCell", owner: self) as! SidebarCell
let node = item as! Node
configure(cell, node)
return cell
}
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(NSArray(object: selectedNode.representedObject))
}
}
//MARK: NSOutlineViewDataSource
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
return nodeForItem(item as AnyObject?).numberOfChildNodes
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
return nodeForItem(item as AnyObject?).childNodes![index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
return nodeForItem(item as AnyObject?).canHaveChildNodes
}
}
//MARK: - Private
private extension SidebarViewController {
var selectedNodes: [Node] {
get {
if let nodes = outlineView.selectedItems as? [Node] {
return nodes
}
return [Node]()
}
}
func rebuildTreeAndReloadDataIfNeeded() {
if !dataModelIsPerformingBatchUpdates() {
treeController.rebuild()
outlineView.reloadData()
}
}
func postSidebarSelectionDidChangeNotification(_ selectedObjects: NSArray?) {
var userInfo = [AnyHashable: Any]()
if let selectedObjects = selectedObjects {
userInfo[objectsKey] = selectedObjects
}
userInfo[viewKey] = self.outlineView
NotificationCenter.default.post(name: .SidebarSelectionDidChange, object: self, userInfo: userInfo)
}
func nodeForItem(_ item: AnyObject?) -> Node {
if item == nil {
return treeController.rootNode
}
return item as! Node
}
func nodeForRow(_ row: Int) -> Node? {
if row < 0 || row >= outlineView.numberOfRows {
return nil
}
if let node = outlineView.item(atRow: row) as? Node {
return node
}
return nil
}
func rowHasAtLeastOneUnreadArticle(_ row: Int) -> Bool {
if let oneNode = nodeForRow(row) {
if let unreadCountProvider = oneNode.representedObject as? UnreadCountProvider {
if unreadCountProvider.unreadCount > 0 {
return true
}
}
}
return false
}
func rowContainingNextUnread() -> Int? {
let selectedRow = outlineView.selectedRow
let numberOfRows = outlineView.numberOfRows
var row = selectedRow + 1
while (row < numberOfRows) {
if rowHasAtLeastOneUnreadArticle(row) {
return row
}
row += 1
}
row = 0
while (row <= selectedRow) {
if rowHasAtLeastOneUnreadArticle(row) {
return row
}
row += 1
}
return nil
}
func configure(_ cell: SidebarCell, _ node: Node) {
cell.objectValue = node
cell.name = nameFor(node)
cell.unreadCount = unreadCountFor(node)
cell.image = imageFor(node)
}
func imageFor(_ node: Node) -> NSImage? {
return nil
}
func nameFor(_ node: Node) -> String {
if let displayNameProvider = node.representedObject as? DisplayNameProvider {
return displayNameProvider.nameForDisplay
}
return ""
}
func unreadCountFor(_ node: Node) -> Int {
if let unreadCountProvider = node.representedObject as? UnreadCountProvider {
return unreadCountProvider.unreadCount
}
return 0
}
func availableSidebarCells() -> [SidebarCell] {
var cells = [SidebarCell]()
outlineView.enumerateAvailableRowViews { (rowView: NSTableRowView, _: Int) -> Void in
if let oneSidebarCell = rowView.view(atColumn: 0) as? SidebarCell {
cells += [oneSidebarCell]
}
}
return cells
}
func cellsForRepresentedObject(_ representedObject: AnyObject) -> [SidebarCell] {
let availableCells = availableSidebarCells()
return availableCells.filter{ (oneSidebarCell) -> Bool in
guard let oneNode = oneSidebarCell.objectValue as? Node else {
return false
}
return oneNode.representedObject === representedObject
}
}
func configureCellsForRepresentedObject(_ representedObject: AnyObject) -> Bool {
//Return true if any cells were configured.
let cells = cellsForRepresentedObject(representedObject)
if cells.isEmpty {
return false
}
cells.forEach { (oneSidebarCell) in
guard let oneNode = oneSidebarCell.objectValue as? Node else {
return
}
configure(oneSidebarCell, oneNode)
oneSidebarCell.needsDisplay = true
oneSidebarCell.needsLayout = true
}
return true
}
@discardableResult
func revealAndSelectRepresentedObject(_ representedObject: AnyObject) -> Bool {
return outlineView.revealAndSelectRepresentedObject(representedObject, treeController)
}
func folderParentForNode(_ node: Node) -> Folder? {
if let folder = node.parent?.representedObject as? Folder {
return folder
}
if let feed = node.representedObject as? Feed {
return feed.account
}
if let folder = node.representedObject as? Folder {
return folder.account
}
return nil
}
func deleteItemForNode(_ node: Node) {
if let folder = folderParentForNode(node) {
folder.deleteItems([node.representedObject])
}
}
func deleteItemsForNodes(_ nodes: [Node]) {
nodes.forEach { (oneNode) in
deleteItemForNode(oneNode)
}
}
}

View File

@ -0,0 +1,105 @@
//
// UnreadCountView.swift
// Evergreen
//
// Created by Brent Simmons on 11/22/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import Cocoa
private let padding = currentTheme.edgeInsets(forKey: "MainWindow.SourceList.unreadCount.padding")
private let cornerRadius = currentTheme.float(forKey: "MainWindow.SourceList.unreadCount.cornerRadius")
private let backgroundColor = currentTheme.colorWithAlpha(forKey: "MainWindow.SourceList.unreadCount.backgroundColor")
private let textColor = currentTheme.colorWithAlpha(forKey: "MainWindow.SourceList.unreadCount.color")
private let textSize = currentTheme.float(forKey: "MainWindow.SourceList.unreadCount.fontSize")
private let textFont = NSFont.systemFont(ofSize: textSize, weight: NSFontWeightSemibold)
private var textAttributes: [String: AnyObject] = [NSForegroundColorAttributeName: textColor, NSFontAttributeName: textFont, NSKernAttributeName: NSNull()]
private var textSizeCache = [Int: NSSize]()
class UnreadCountView : NSView {
var unreadCount = 0 {
didSet {
invalidateIntrinsicContentSize()
needsDisplay = true
}
}
var unreadCountString: String {
get {
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
}
return _intrinsicContentSize
}
}
override var isFlipped: Bool {
get {
return true
}
}
override func invalidateIntrinsicContentSize() {
intrinsicContentSizeIsValid = false
}
private func textSize() -> NSSize {
if unreadCount < 1 {
return NSZeroSize
}
if let cachedSize = textSizeCache[unreadCount] {
return cachedSize
}
var size = unreadCountString.size(withAttributes: textAttributes)
size.height = ceil(size.height)
size.width = ceil(size.width)
textSizeCache[unreadCount] = size
return size
}
private func textRect() -> NSRect {
let size = textSize()
var r = NSZeroRect
r.size = size
r.origin.x = (NSMaxX(bounds) - padding.right) - r.size.width
r.origin.y = padding.top
return r
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath(roundedRect: bounds, xRadius: cornerRadius, yRadius: cornerRadius)
backgroundColor.setFill()
path.fill()
if unreadCount > 0 {
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
}
}
}

View File

@ -0,0 +1,160 @@
//
// StatusBarView.swift
// Evergreen
//
// Created by Brent Simmons on 9/17/16.
// Copyright © 2016 Ranchero Software. All rights reserved.
//
import Cocoa
import RSCore
import DataModel
import RSWeb
final class StatusBarView: NSView {
@IBOutlet var progressIndicator: NSProgressIndicator!
@IBOutlet var progressLabel: NSTextField!
@IBOutlet var urlLabel: NSTextField!
fileprivate var isAnimatingProgress = false
fileprivate var article: Article? {
didSet {
updateURLLabel()
}
}
override var isFlipped: Bool {
get {
return true
}
}
override func awakeFromNib() {
let progressLabelFontSize = progressLabel.font?.pointSize ?? 13.0
progressLabel.font = NSFont.monospacedDigitSystemFont(ofSize: progressLabelFontSize, weight: NSFontWeightRegular)
progressLabel.stringValue = ""
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(timelineSelectionDidChange(_:)), name: .TimelineSelectionDidChange, object: nil)
}
// MARK: Notifications
dynamic func progressDidChange(_ notification: Notification) {
guard let progress = notification.userInfo?[progressKey] as? DownloadProgress else {
return
}
updateProgressIndicator(progress)
updateProgressLabel(progress)
}
// MARK: Notifications
dynamic func timelineSelectionDidChange(_ note: Notification) {
let timelineView = note.userInfo?[viewKey] as! NSView
if timelineView.window! === self.window {
article = note.userInfo?[articleKey] as? Article
}
}
// MARK: Drawing
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()
}
}
private extension StatusBarView {
// MARK: URL Label
func updateURLLabel() {
needsLayout = true
guard let article = article else {
urlLabel.stringValue = ""
return
}
let s = preferredLink(for: article)
if let s = s {
urlLabel.stringValue = (s as NSString).rs_stringByStrippingHTTPOrHTTPSScheme()
}
else {
urlLabel.stringValue = ""
}
}
// MARK: Progress
func stopProgressIfNeeded() {
if !isAnimatingProgress {
return
}
progressIndicator.stopAnimation(self)
isAnimatingProgress = false
progressIndicator.needsDisplay = true
}
func startProgressIfNeeded() {
if isAnimatingProgress {
return
}
isAnimatingProgress = true
progressIndicator.startAnimation(self)
}
func updateProgressIndicator(_ progress: DownloadProgress) {
if progress.isComplete {
stopProgressIfNeeded()
return
}
startProgressIfNeeded()
let maxValue = Double(progress.numberOfTasks)
if progressIndicator.maxValue != maxValue {
progressIndicator.maxValue = maxValue
}
let doubleValue = Double(progress.numberCompleted)
if progressIndicator.doubleValue != doubleValue {
progressIndicator.doubleValue = doubleValue
}
}
func updateProgressLabel(_ progress: DownloadProgress) {
if progress.isComplete {
progressLabel.stringValue = ""
return
}
let numberOfTasks = progress.numberOfTasks
let numberCompleted = progress.numberCompleted
let formatString = NSLocalizedString("%@ of %@", comment: "Status bar progress")
let s = NSString(format: formatString as NSString, NSNumber(value: numberCompleted), NSNumber(value: numberOfTasks))
progressLabel.stringValue = s as String
}
}

View File

@ -0,0 +1,121 @@
//
// TimelineCellAppearance.swift
// Evergreen
//
// Created by Brent Simmons on 2/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
import DB5
struct TimelineCellAppearance {
let cellPadding: EdgeInsets
let feedNameColor: NSColor
let feedNameFont: NSFont
let faviconFeedNameSpacing: CGFloat
let faviconSize = NSSize(width: 16, height: 16)
let dateColor: NSColor
let dateMarginLeft: CGFloat
let dateFont: NSFont
let titleColor: NSColor
let titleFont: NSFont
let titleBottomMargin: CGFloat
let textColor: NSColor
let textFont: NSFont
let unreadCircleColor: NSColor
let unreadCircleDimension: CGFloat
let unreadCircleMarginRight: CGFloat
let boxLeftMargin: CGFloat
let gridColor: NSColor
init(theme: VSTheme, fontSize: FontSize) {
let actualFontSize = actualFontSizeForFontSize(fontSize)
cellPadding = theme.edgeInsets(forKey: "MainWindow.Timeline.cell.padding")
feedNameColor = theme.color(forKey: "MainWindow.Timeline.cell.feedNameColor")
feedNameFont = NSFont.systemFont(ofSize: actualFontSize)
faviconFeedNameSpacing = theme.float(forKey: "MainWindow.Timeline.cell.faviconFeedNameSpacing")
dateColor = theme.color(forKey: "MainWindow.Timeline.cell.dateColor")
let actualDateFontSize = actualDateFontSizeForFontSize(fontSize)
dateFont = NSFont.systemFont(ofSize: actualDateFontSize)
dateMarginLeft = theme.float(forKey: "MainWindow.Timeline.cell.dateMarginLeft")
titleColor = theme.color(forKey: "MainWindow.Timeline.cell.titleColor")
titleFont = NSFont.systemFont(ofSize: actualFontSize, weight: NSFontWeightBold)
titleBottomMargin = theme.float(forKey: "MainWindow.Timeline.cell.titleMarginBottom")
textColor = theme.color(forKey: "MainWindow.Timeline.cell.textColor")
textFont = NSFont.systemFont(ofSize: actualFontSize)
unreadCircleColor = theme.color(forKey: "MainWindow.Timeline.cell.unreadCircleColor")
unreadCircleDimension = theme.float(forKey: "MainWindow.Timeline.cell.unreadCircleDimension")
unreadCircleMarginRight = theme.float(forKey: "MainWindow.Timeline.cell.unreadCircleMarginRight")
boxLeftMargin = cellPadding.left + unreadCircleDimension + unreadCircleMarginRight
gridColor = theme.colorWithAlpha(forKey: "MainWindow.Timeline.gridColor")
}
}
private let smallFontSize = NSFont.systemFontSize()
private let mediumFontSize = smallFontSize + 1.0
private let largeFontSize = mediumFontSize + 4.0
private let veryLargeFontSize = largeFontSize + 8.0
private func actualFontSizeForFontSize(_ fontSize: FontSize) -> CGFloat {
var actualFontSize = smallFontSize
switch (fontSize) {
case .small:
actualFontSize = smallFontSize
case .medium:
actualFontSize = mediumFontSize
case .large:
actualFontSize = largeFontSize
case .veryLarge:
actualFontSize = veryLargeFontSize
}
return actualFontSize
}
//private let smallDateFontSize = NSFont.systemFontSize() - 2.0
//private let mediumDateFontSize = smallDateFontSize + 1.0
//private let largeDateFontSize = mediumDateFontSize + 4.0
//private let veryLargeDateFontSize = largeDateFontSize + 8.0
private func actualDateFontSizeForFontSize(_ fontSize: FontSize) -> CGFloat {
return actualFontSizeForFontSize(fontSize)
// var actualFontSize = smallDateFontSize
//
// switch (fontSize) {
//
// case .small:
// actualFontSize = smallDateFontSize
// case .medium:
// actualFontSize = mediumDateFontSize
// case .large:
// actualFontSize = largeDateFontSize
// case .veryLarge:
// actualFontSize = veryLargeDateFontSize
// }
//
// return actualFontSize
}

View File

@ -0,0 +1,118 @@
//
// TimelineCellData.swift
// Evergreen
//
// Created by Brent Simmons on 2/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
import DataModel
var attributedTitleCache = [String: NSAttributedString]()
var attributedDateCache = [String: NSAttributedString]()
var attributedFeedNameCache = [String: NSAttributedString]()
struct TimelineCellData {
let title: String
let text: String
let attributedTitle: NSAttributedString //title + text
let dateString: String
let attributedDateString: NSAttributedString
let feedName: String
let attributedFeedName: NSAttributedString
let showFeedName: Bool
let favicon: NSImage?
let read: Bool
init(article: Article, appearance: TimelineCellAppearance, showFeedName: Bool) {
self.title = timelineTruncatedTitle(article)
self.text = timelineTruncatedSummary(article)
let attributedTitleCacheKey = "_title: " + self.title + "_text: " + self.text
if let s = attributedTitleCache[attributedTitleCacheKey] {
self.attributedTitle = s
}
else {
self.attributedTitle = attributedTitleString(title, text, appearance)
attributedTitleCache[attributedTitleCacheKey] = self.attributedTitle
}
self.dateString = timelineDateString(article.logicalDatePublished)
if let s = attributedDateCache[self.dateString] {
self.attributedDateString = s
}
else {
self.attributedDateString = NSAttributedString(string: self.dateString, attributes: [NSForegroundColorAttributeName: appearance.dateColor, NSFontAttributeName: appearance.dateFont])
attributedDateCache[self.dateString] = self.attributedDateString
}
if let feed = article.feed {
self.feedName = timelineTruncatedFeedName(feed)
}
else {
self.feedName = ""
}
if let s = attributedFeedNameCache[self.dateString] {
self.attributedFeedName = s
}
else {
self.attributedFeedName = NSAttributedString(string: self.feedName, attributes: [NSForegroundColorAttributeName: appearance.feedNameColor, NSFontAttributeName: appearance.feedNameFont])
attributedFeedNameCache[self.feedName] = self.attributedFeedName
}
self.showFeedName = showFeedName
self.favicon = nil
if let status = article.status {
self.read = status.read
}
else {
self.read = false
}
}
init() { //Empty
self.title = ""
self.attributedTitle = NSAttributedString(string: "")
self.text = ""
self.dateString = ""
self.attributedDateString = NSAttributedString(string: "")
self.feedName = ""
self.attributedFeedName = NSAttributedString(string: "")
self.showFeedName = false
self.favicon = nil
self.read = true
}
static func emptyCache() {
attributedTitleCache = [String: NSAttributedString]()
attributedDateCache = [String: NSAttributedString]()
attributedFeedNameCache = [String: NSAttributedString]()
}
}
let emptyCellData = TimelineCellData()
private func attributedTitleString(_ title: String, _ text: String, _ appearance: TimelineCellAppearance) -> NSAttributedString {
if !title.isEmpty && !text.isEmpty {
let titleMutable = NSMutableAttributedString(string: title, attributes: [NSForegroundColorAttributeName: appearance.titleColor, NSFontAttributeName: appearance.titleFont])
let attributedText = NSAttributedString(string: "\n" + text, attributes: [NSForegroundColorAttributeName: appearance.textColor, NSFontAttributeName: appearance.textFont])
titleMutable.append(attributedText)
return titleMutable
}
if !title.isEmpty && text.isEmpty {
return NSAttributedString(string: title, attributes: [NSForegroundColorAttributeName: appearance.titleColor, NSFontAttributeName: appearance.titleFont])
}
return NSAttributedString(string: text, attributes: [NSForegroundColorAttributeName: appearance.textColor, NSFontAttributeName: appearance.textFont])
}

View File

@ -0,0 +1,156 @@
//
// TimelineCellLayout.swift
// Evergreen
//
// Created by Brent Simmons on 2/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
import RSTextDrawing
import RSCore
// title/text 1 date
// title/text 2
// title/text 3
// favicon feedname (optional line)
struct TimelineCellLayout {
let width: CGFloat
let height: CGFloat
let faviconRect: NSRect
let feedNameRect: NSRect
let dateRect: NSRect
let titleRect: NSRect
let unreadIndicatorRect: NSRect
let paddingBottom: CGFloat
init(width: CGFloat, faviconRect: NSRect, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, unreadIndicatorRect: NSRect, paddingBottom: CGFloat) {
self.width = width
self.faviconRect = faviconRect
self.feedNameRect = feedNameRect
self.dateRect = dateRect
self.titleRect = titleRect
self.unreadIndicatorRect = unreadIndicatorRect
self.paddingBottom = paddingBottom
var height = NSMaxY(dateRect)
if feedNameRect != NSZeroRect {
height = NSMaxY(feedNameRect)
}
height = height + paddingBottom
self.height = 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
// r.origin.y = appearance.cellPadding.top
// r.origin.x = width - (appearance.cellPadding.right + r.size.width)
r.origin.y = NSMaxY(titleRect) + appearance.titleBottomMargin
r.origin.x = appearance.boxLeftMargin
r.size.width = width - (r.origin.x + appearance.cellPadding.right)
if r.size.width < 15 {
return NSZeroRect
}
return r
}
private func rectForFeedName(_ cellData: TimelineCellData, _ width: CGFloat, _ appearance: TimelineCellAppearance, _ titleRect: NSRect) -> NSRect {
if !cellData.showFeedName {
return NSZeroRect
}
let renderer = RSSingleLineRenderer(attributedTitle: cellData.attributedFeedName)
var r = NSZeroRect
r.size = renderer.size
r.origin.y = NSMaxY(titleRect) + appearance.titleBottomMargin
r.origin.x = appearance.boxLeftMargin
if let _ = cellData.favicon {
r.origin.x += appearance.faviconSize.width + appearance.faviconFeedNameSpacing
}
r.size.width = width - (r.origin.x + appearance.cellPadding.right)
if r.size.width < 15 {
return NSZeroRect
}
return r
}
private func rectForFavicon(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ feedNameRect: NSRect) -> NSRect {
guard let _ = cellData.favicon, cellData.showFeedName else {
return NSZeroRect
}
var r = NSZeroRect
r.size = appearance.faviconSize
r.origin.x = appearance.boxLeftMargin
r = RSRectCenteredVerticallyInRect(r, feedNameRect)
return r
}
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 textWidth = width - (r.origin.x + appearance.cellPadding.right)
let renderer = RSMultiLineRenderer(attributedTitle: cellData.attributedTitle)
let measurements = renderer.measurements(forWidth: textWidth)
r.size = NSSize(width: textWidth, height: CGFloat(measurements.height))
var rline1 = r
rline1.size.height = CGFloat(measurements.heightOfFirstLine)
return (r, rline1)
}
private func rectForUnreadIndicator(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ titleLine1Rect: NSRect) -> NSRect {
if cellData.read {
return NSZeroRect
}
var r = NSZeroRect
r.size = NSSize(width: appearance.unreadCircleDimension, height: appearance.unreadCircleDimension)
r.origin.x = appearance.cellPadding.left
r = RSRectCenteredVerticallyInRect(r, titleLine1Rect)
return r
}
func timelineCellLayout(_ width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) -> TimelineCellLayout {
// let dateRect = rectForDate(cellData, width, appearance)
let (titleRect, titleLine1Rect) = rectsForTitle(cellData, width, appearance)
let dateRect = rectForDate(cellData, width, appearance, titleRect)
let feedNameRect = rectForFeedName(cellData, width, appearance, titleRect)
let faviconRect = rectForFavicon(cellData, appearance, feedNameRect)
let unreadIndicatorRect = rectForUnreadIndicator(cellData, appearance, titleLine1Rect)
return TimelineCellLayout(width: width, faviconRect: faviconRect, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, unreadIndicatorRect: unreadIndicatorRect, paddingBottom: appearance.cellPadding.bottom)
}
func timelineCellHeight(_ width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) -> CGFloat {
let layout = timelineCellLayout(width, cellData: cellData, appearance: appearance)
return layout.height
}

View File

@ -0,0 +1,162 @@
//
// TimelineStringUtilities.swift
// Evergreen
//
// Created by Brent Simmons on 8/31/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import DataModel
private let truncatedFeedNameCache = NSMutableDictionary()
private let truncatedTitleCache = NSMutableDictionary()
private let normalizedTextCache = NSMutableDictionary()
private let textCache = NSMutableDictionary()
private let summaryCache = NSMutableDictionary()
//private var summaryCache = [String: String]()
func timelineEmptyCaches() {
truncatedFeedNameCache.removeAllObjects()
truncatedTitleCache.removeAllObjects()
normalizedTextCache.removeAllObjects()
textCache.removeAllObjects()
summaryCache.removeAllObjects()
}
func timelineTruncatedFeedName(_ feed: Feed) -> String {
let feedName = feed.nameForDisplay
if let cachedFeedName = truncatedFeedNameCache[feedName] as? String {
return cachedFeedName
}
let maxFeedNameLength = 100
if feedName.characters.count < maxFeedNameLength {
truncatedFeedNameCache[feedName] = feedName
return feedName
}
let s = (feedName as NSString).substring(to: maxFeedNameLength)
truncatedFeedNameCache[feedName] = s
return s
}
func timelineTruncatedTitle(_ article: Article) -> String {
guard let title = article.title else {
return ""
}
if let cachedTitle = truncatedTitleCache[title] as? String {
return cachedTitle
}
var s = title.replacingOccurrences(of: "\n", with: "")
s = s.replacingOccurrences(of: "\r", with: "")
s = s.replacingOccurrences(of: "\t", with: "")
s = s.replacingOccurrences(of: "", with: "")
s = s.rs_stringByTrimmingWhitespace()
let maxLength = 1000
if s.characters.count < maxLength {
truncatedTitleCache[title] = s
return s
}
s = (s as NSString).substring(to: maxLength)
truncatedTitleCache[title] = s
return s
}
func timelineTruncatedSummary(_ article: Article) -> String {
return timelineSummaryForArticle(article)
}
func timelineNormalizedText(_ text: String) -> String {
if text.isEmpty {
return ""
}
if let cachedText = normalizedTextCache[text] as? String {
return cachedText
}
var s = (text as NSString).rs_stringByTrimmingWhitespace()
s = s.rs_stringWithCollapsedWhitespace()
let result = s as String
normalizedTextCache[text] = result
return result
}
func timelineNormalizedTextTruncated(_ text: String) -> String {
if text.isEmpty {
return ""
}
if let cachedText = textCache[text] as? String {
return cachedText
}
var s: NSString = (text as NSString).rs_stringByDecodingHTMLEntities() as NSString
s = s.rs_stringByTrimmingWhitespace() as NSString
s = s.rs_stringWithCollapsedWhitespace() as NSString
let maxLength = 512
if s.length > maxLength {
s = s.substring(to: maxLength) as NSString
}
textCache[text] = String(s)
return s as String
}
func timelineSummaryForArticle(_ article: Article) -> String {
guard let body = article.body else {
return ""
}
if let cachedBody = summaryCache[body] as? String {
return cachedBody
}
var s = body.rs_string(byStrippingHTML: 300)
s = timelineNormalizedText(s)
summaryCache[body] = s
return s
}
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
private let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}()
private var token: Int = 0
func timelineDateString(_ date: Date) -> String {
if NSCalendar.rs_dateIsToday(date) {
return timeFormatter.string(from: date)
}
return dateFormatter.string(from: date)
}

View File

@ -0,0 +1,164 @@
//
// TimelineTableCellView.swift
// Evergreen
//
// Created by Brent Simmons on 8/31/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
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)
var cellAppearance: TimelineCellAppearance!
var cellData: TimelineCellData! {
didSet {
updateSubviews()
}
}
override var isFlipped: Bool {
return true
}
var isEmphasized = false {
didSet {
dateView.emphasized = isEmphasized
feedNameView.emphasized = isEmphasized
titleView.emphasized = isEmphasized
}
}
var isSelected = false {
didSet {
dateView.selected = isSelected
feedNameView.selected = isSelected
titleView.selected = isSelected
}
}
private func commonInit() {
addSubview(titleView)
titleView.translatesAutoresizingMaskIntoConstraints = false
addSubview(unreadIndicatorView)
unreadIndicatorView.translatesAutoresizingMaskIntoConstraints = false
unreadIndicatorView.isHidden = true
addSubview(dateView)
dateView.translatesAutoresizingMaskIntoConstraints = false
addSubview(feedNameView)
feedNameView.translatesAutoresizingMaskIntoConstraints = false;
feedNameView.isHidden = true
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func setFrameSize(_ newSize: NSSize) {
if newSize == self.frame.size {
return
}
super.setFrameSize(newSize)
updateSubviews()
needsLayout = true
}
override func viewDidMoveToSuperview() {
updateSubviews()
updateAppearance()
}
private func updatedLayoutRects() -> TimelineCellLayout {
return timelineCellLayout(NSWidth(bounds), cellData: cellData, appearance: cellAppearance)
}
override func layout() {
resizeSubviews(withOldSize: NSZeroSize)
}
override func resizeSubviews(withOldSize oldSize: NSSize) {
let layoutRects = updatedLayoutRects()
titleView.rs_setFrameIfNotEqual(layoutRects.titleRect)
unreadIndicatorView.rs_setFrameIfNotEqual(layoutRects.unreadIndicatorRect)
dateView.rs_setFrameIfNotEqual(layoutRects.dateRect)
feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect)
}
private func updateTitleView() {
titleView.attributedStringValue = cellData.attributedTitle
needsLayout = true
}
private func updateDateView() {
dateView.attributedStringValue = cellData.attributedDateString
needsLayout = true
}
private func updateFeedNameView() {
if cellData.showFeedName {
if feedNameView.isHidden {
feedNameView.isHidden = false
}
feedNameView.attributedStringValue = cellData.attributedFeedName
}
else {
if !feedNameView.isHidden {
feedNameView.isHidden = true
}
}
}
private func updateUnreadIndicator() {
if unreadIndicatorView.isHidden != cellData.read {
unreadIndicatorView.isHidden = cellData.read
}
}
private func updateSubviews() {
updateTitleView()
updateDateView()
updateFeedNameView()
updateUnreadIndicator()
}
private func updateAppearance() {
if let rowView = superview as? NSTableRowView {
isEmphasized = rowView.isEmphasized
isSelected = rowView.isSelected
}
else {
isEmphasized = false
isSelected = false
}
}
}

View File

@ -0,0 +1,28 @@
//
// UnreadIndicatorView.swift
// Evergreen
//
// Created by Brent Simmons on 2/16/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
class UnreadIndicatorView: NSView {
static let unreadCircleDimension = currentTheme.float(forKey: "MainWindow.Timeline.cell.unreadCircleDimension")
static let bezierPath: NSBezierPath = {
let r = NSRect(x: 0.0, y: 0.0, width: unreadCircleDimension, height: unreadCircleDimension)
return NSBezierPath(ovalIn: r)
}()
static let unreadCircleColor = currentTheme.color(forKey: "MainWindow.Timeline.cell.unreadCircleColor")
override func draw(_ dirtyRect: NSRect) {
UnreadIndicatorView.unreadCircleColor.setFill()
UnreadIndicatorView.bezierPath.fill()
}
}

View File

@ -0,0 +1,78 @@
//
// TimelineTableRowView.swift
// Evergreen
//
// Created by Brent Simmons on 8/31/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
class TimelineTableRowView : NSTableRowView {
var cellAppearance: TimelineCellAppearance!
// override var interiorBackgroundStyle: NSBackgroundStyle {
// return .Light
// }
private var cellView: TimelineTableCellView? {
get {
for oneSubview in subviews {
if let foundView = oneSubview as? TimelineTableCellView {
return foundView
}
}
return nil
}
}
override var isEmphasized: Bool {
didSet {
if let cellView = cellView {
cellView.isEmphasized = isEmphasized
}
}
}
override var isSelected: Bool {
didSet {
if let cellView = cellView {
cellView.isSelected = isSelected
}
}
}
var gridRect: NSRect {
get {
return NSMakeRect(floor(cellAppearance.boxLeftMargin), NSMaxY(bounds) - 1.0, NSWidth(bounds), 1)
}
}
override func drawSeparator(in dirtyRect: NSRect) {
let path = NSBezierPath()
let originX = floor(cellAppearance.boxLeftMargin)
let destinationX = ceil(NSMaxX(bounds))
let y = floor(NSMaxY(bounds)) - 0.5
path.move(to: NSPoint(x: originX, y: y))
path.line(to: NSPoint(x: destinationX, y: y))
cellAppearance.gridColor.set()
path.stroke()
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
if !isSelected && !isNextRowSelected {
drawSeparator(in: dirtyRect)
}
}
func invalidateGridRect() {
setNeedsDisplay(gridRect)
}
}

View File

@ -0,0 +1,44 @@
//
// TimelineTableView.swift
// Evergreen
//
// Created by Brent Simmons on 10/11/16.
// Copyright © 2016 Ranchero Software. All rights reserved.
//
import Cocoa
class TimelineTableView: NSTableView {
weak var keyboardDelegate: KeyboardDelegate?
//MARK: NSResponder
override func keyDown(with event: NSEvent) {
if let keyboardDelegate = keyboardDelegate {
if keyboardDelegate.handleKeydownEvent(event, sender: self) {
return;
}
}
super.keyDown(with: event)
}
override func viewWillStartLiveResize() {
if let scrollView = self.enclosingScrollView {
scrollView.hasVerticalScroller = false
}
super.viewWillStartLiveResize()
}
override func viewDidEndLiveResize() {
if let scrollView = self.enclosingScrollView {
scrollView.hasVerticalScroller = true
}
super.viewDidEndLiveResize()
}
}

View File

@ -0,0 +1,625 @@
//
// TimelineViewController.swift
// Evergreen
//
// Created by Brent Simmons on 7/26/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSTextDrawing
import RSTree
import DataModel
import LocalAccount
let timelineFontSizeKVOKey = "values." + TimelineFontSizeKey
class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, KeyboardDelegate {
@IBOutlet var tableView: TimelineTableView!
var didRegisterForNotifications = false
var fontSize: FontSize = timelineFontSize() {
didSet {
fontSizeDidChange()
}
}
var cellAppearance: TimelineCellAppearance!
private var articles = [Article]() {
didSet {
tableView.reloadData()
}
}
private var representedObjects: [AnyObject]? {
didSet {
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
fetchArticles()
if articles.count > 0 {
tableView.scrollRowToVisible(0)
}
}
}
}
private var showFeedNames: Bool {
// if let _ = node?.representedObject as? Feed {
return false
// }
// return true
}
var selectedArticles: [Article] {
get {
return articlesForIndexes(tableView.selectedRowIndexes)
}
}
private var oneSelectedArticle: Article? {
get {
return selectedArticles.count == 1 ? selectedArticles.first : nil
}
}
override func viewDidLoad() {
cellAppearance = TimelineCellAppearance(theme: currentTheme, fontSize: fontSize)
tableView.rowHeight = calculateRowHeight()
tableView.target = self
tableView.doubleAction = #selector(openArticleInBrowser(_:))
tableView.keyboardDelegate = self
if !didRegisterForNotifications {
NotificationCenter.default.addObserver(self, selector: #selector(sidebarSelectionDidChange(_:)), name: .SidebarSelectionDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(articleStatusesDidChange(_:)), name: .ArticleStatusesDidChange, object: nil)
NSUserDefaultsController.shared().addObserver(self, forKeyPath:timelineFontSizeKVOKey, options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
didRegisterForNotifications = true
}
}
// MARK: KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let keyPath = keyPath {
switch (keyPath) {
case timelineFontSizeKVOKey:
fontSizeInDefaultsDidChange()
return
default:
break
}
}
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
// MARK: Appearance Change
private func fontSizeDidChange() {
cellAppearance = TimelineCellAppearance(theme: currentTheme, fontSize: fontSize)
let updatedRowHeight = calculateRowHeight()
if tableView.rowHeight != updatedRowHeight {
tableView.rowHeight = updatedRowHeight
tableView.reloadData()
}
}
// MARK: API
func markAllAsRead() {
if articles.isEmpty {
return
}
let articlesSet = NSMutableSet()
articlesSet.addObjects(from: articles)
markArticles(articlesSet, statusKey: .read, flag: true)
reloadCellsForArticles(articles)
}
// MARK: Actions
func openArticleInBrowser(_ sender: AnyObject) {
guard let article = oneSelectedArticle else {
return
}
if let link = preferredLink(for: article) {
openInBrowser(link)
}
}
@IBAction func toggleStatusOfSelectedArticles(_ sender: AnyObject) {
guard !selectedArticles.isEmpty else {
return
}
let articles = selectedArticles
var markAsRead = true
if articles.first!.status.read {
markAsRead = false
}
markArticles(NSSet(array: articles), statusKey: .read, flag: markAsRead)
}
@IBAction func markSelectedArticlesAsRead(_ sender: AnyObject) {
markArticles(NSSet(array: selectedArticles), statusKey: .read, flag: true)
}
@IBAction func markSelectedArticlesAsUnread(_ sender: AnyObject) {
markArticles(NSSet(array: selectedArticles), statusKey: .read, flag: false)
}
// MARK: Navigation
func goToNextUnread() {
guard let ix = indexOfNextUnreadArticle() else {
return
}
tableView.rs_selectRow(ix)
tableView.scrollTo(row: ix)
// tableView.rs_selectRowAndScrollToVisible(ix)
}
func canGoToNextUnread() -> Bool {
guard let _ = indexOfNextUnreadArticle() else {
return false
}
return true
}
func canMarkAllAsRead() -> Bool {
for oneArticle in articles {
if !oneArticle.status.read {
return true
}
}
return false
}
func indexOfNextUnreadArticle() -> Int? {
if articles.isEmpty {
return nil
}
var ix = tableView.selectedRow
while(true) {
ix = ix + 1
if ix >= articles.count {
break
}
let article = articleAtRow(ix)!
if !article.status.read {
return ix
}
}
return nil
}
// MARK: Notifications
func sidebarSelectionDidChange(_ note: Notification) {
let sidebarView = note.userInfo?[viewKey] as! NSView
if sidebarView.window! === tableView.window {
representedObjects = note.userInfo?[objectsKey] as? [AnyObject]
}
}
func articleStatusesDidChange(_ note: Notification) {
guard let articles = note.userInfo?[articlesKey] as? NSSet else {
return
}
reloadCellsForArticles(articles.allObjects as! [Article])
}
func fontSizeInDefaultsDidChange() {
TimelineCellData.emptyCache()
RSSingleLineRenderer.emptyCache()
RSMultiLineRenderer.emptyCache()
let updatedFontSize = timelineFontSize()
if updatedFontSize != self.fontSize {
self.fontSize = updatedFontSize
}
}
// MARK: KeyboardDelegate
func handleKeydownEvent(_ event: NSEvent, sender: AnyObject) -> Bool {
guard !event.rs_keyIsModified() else {
return false
}
guard let ch = event.rs_unmodifiedCharacterString() else {
return false
}
let hasSelectedArticle = hasAtLeastOneSelectedArticle
var keyHandled = false
var shouldOpenInBrowser = false
switch(ch) {
case "\n":
shouldOpenInBrowser = true
keyHandled = true
case "\r":
shouldOpenInBrowser = true
keyHandled = true
case "r":
markSelectedArticlesAsRead(sender)
keyHandled = true
case "u":
markSelectedArticlesAsUnread(sender)
keyHandled = true
default:
keyHandled = false
}
if !keyHandled {
let chUnichar = event.rs_unmodifiedCharacter()
switch(chUnichar) {
case keypadEnter:
shouldOpenInBrowser = true
keyHandled = true
default:
keyHandled = false
}
}
if shouldOpenInBrowser && hasSelectedArticle {
openArticleInBrowser(self)
}
return keyHandled
}
// MARK: Reloading Data
private func cellForRowView(_ rowView: NSView) -> NSView? {
for oneView in rowView.subviews where oneView is TimelineTableCellView {
return oneView
}
return nil
}
private func reloadCellsForArticles(_ articles: [Article]) {
let indexes = indexesForArticles(articles)
tableView.reloadData(forRowIndexes: indexes, columnIndexes: NSIndexSet(index: 0) as IndexSet)
}
// MARK: Articles
private func indexesForArticles(_ articles: [Article]) -> IndexSet {
var indexes = IndexSet()
articles.forEach { (article) in
let oneIndex = rowForArticle(article)
if oneIndex != NSNotFound {
indexes.insert(oneIndex)
}
}
return indexes
}
private func articlesForIndexes(_ indexes: IndexSet) -> [Article] {
return indexes.flatMap{ (oneIndex) -> Article? in
return articleAtRow(oneIndex)
}
}
private func articleAtRow(_ row: Int) -> Article? {
if row < 0 || row == NSNotFound || row > articles.count - 1 {
return nil
}
return articles[row]
}
private func rowForArticle(_ article: Article) -> Int {
if let index = articles.index(where: { (oneArticle) -> Bool in
return oneArticle === article
}) {
return index
}
return NSNotFound
}
func selectedArticle() -> Article? {
return articleAtRow(tableView.selectedRow)
}
// MARK: Sorting Articles
private func articleComparator(_ article1: Article, article2: Article) -> Bool {
return article1.logicalDatePublished.compare(article2.logicalDatePublished) == .orderedDescending
}
// MARK: Fetching Articles
private func fetchArticles() {
guard let representedObjects = representedObjects else {
if !articles.isEmpty {
articles = [Article]()
}
return
}
var accountsDictionary = [String: [AnyObject]]()
func addToAccountArray(accountID: String, object: AnyObject) {
if let accountArray = accountsDictionary[accountID] {
if !accountArray.contains(where: { $0 === object }) {
accountsDictionary[accountID] = accountArray + [object]
}
}
else {
accountsDictionary[accountID] = [object]
}
}
for oneObject in representedObjects {
if let oneFeed = oneObject as? Feed {
addToAccountArray(accountID: oneFeed.account.identifier, object: oneFeed)
}
else if let oneFolder = oneObject as? Folder, let accountID = oneFolder.account?.identifier {
addToAccountArray(accountID: accountID, object: oneFolder)
}
}
var fetchedArticles = [Article]()
for (accountID, objects) in accountsDictionary {
guard let oneAccount = AccountManager.sharedInstance.existingAccountWithIdentifier(accountID) else {
continue
}
let oneFetchedArticles = oneAccount.fetchArticles(for: objects)
for oneFetchedArticle in oneFetchedArticles {
if !fetchedArticles.contains(where: { $0 === oneFetchedArticle }) {
fetchedArticles += [oneFetchedArticle]
}
}
}
fetchedArticles.sort(by: articleComparator)
if !articleArraysAreIdentical(array1: articles, array2: fetchedArticles) {
articles = fetchedArticles
}
}
// MARK: Cell Configuring
private func calculateRowHeight() -> CGFloat {
let prototypeArticle = LocalArticle(account: AccountManager.sharedInstance.localAccount, feedID: "prototype", articleID: "prototype")
prototypeArticle.title = "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
let prototypeArticleStatus = LocalArticleStatus(articleID: "prototype", read: false, starred: false, userDeleted: false, dateArrived: Date())
prototypeArticle.status = prototypeArticleStatus
let prototypeCellData = TimelineCellData(article: prototypeArticle, appearance: cellAppearance, showFeedName: false)
let height = timelineCellHeight(100, cellData: prototypeCellData, appearance: cellAppearance)
return height
}
private func configureTimelineCell(_ cell: TimelineTableCellView, article: Article) {
cell.objectValue = article
cell.cellData = TimelineCellData(article: article, appearance: cellAppearance, showFeedName: showFeedNames)
}
private func makeTimelineCellEmpty(_ cell: TimelineTableCellView) {
cell.objectValue = nil
cell.cellData = emptyCellData
}
// MARK: NSTableViewDataSource
func numberOfRows(in tableView: NSTableView) -> Int {
return articles.count
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return articleAtRow(row)
}
// MARK: NSTableViewDelegate
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let rowView: TimelineTableRowView = tableView.make(withIdentifier: "timelineRow", owner: self) as! TimelineTableRowView
rowView.cellAppearance = cellAppearance
return rowView
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell: TimelineTableCellView = tableView.make(withIdentifier: "timelineCell", owner: self) as! TimelineTableCellView
cell.cellAppearance = cellAppearance
if let article = articleAtRow(row) {
configureTimelineCell(cell, article: article)
}
else {
makeTimelineCellEmpty(cell)
}
return cell
}
private func postTimelineSelectionDidChangeNotification(_ selectedArticle: Article?) {
var userInfo = [String: AnyObject]()
if let article = selectedArticle {
userInfo[articleKey] = article
}
userInfo[viewKey] = self.tableView
NotificationCenter.default.post(name: .TimelineSelectionDidChange, object: self, userInfo: userInfo)
}
func tableViewSelectionDidChange(_ notification: Notification) {
tableView.redrawGrid()
let selectedRow = tableView.selectedRow
if selectedRow < 0 || selectedRow == NSNotFound || tableView.numberOfSelectedRows != 1 {
postTimelineSelectionDidChangeNotification(nil)
return
}
if let selectedArticle = articleAtRow(selectedRow) {
let articleSet = NSSet(array: [selectedArticle])
if (!selectedArticle.status.read) {
markArticles(articleSet, statusKey: .read, flag: true)
}
postTimelineSelectionDidChangeNotification(selectedArticle)
}
else {
postTimelineSelectionDidChangeNotification(nil)
}
}
func representedObjectArraysAreEqual(_ objects1: [AnyObject]?, _ objects2: [AnyObject]?) -> Bool {
if objects1 == nil && objects2 == nil {
return true
}
guard let objects1 = objects1, let objects2 = objects2 else {
return false
}
if objects1.count != objects2.count {
return false
}
var ix = 0
for oneObject in objects1 {
if oneObject !== objects2[ix] {
return false
}
ix += 1
}
return true
}
}
private extension TimelineViewController {
var hasAtLeastOneSelectedArticle: Bool {
get {
return self.tableView.selectedRow != -1
}
}
}
private extension NSTableView {
func scrollTo(row: Int) {
guard let scrollView = self.enclosingScrollView else {
return
}
let documentVisibleRect = scrollView.documentVisibleRect
let r = rect(ofRow: row)
if NSContainsRect(documentVisibleRect, r) {
return
}
let rMidY = NSMidY(r)
var scrollPoint = NSZeroPoint;
let extraHeight = 150
scrollPoint.y = floor(rMidY - (documentVisibleRect.size.height / 2.0)) + CGFloat(extraHeight)
scrollPoint.y = max(scrollPoint.y, 0)
let maxScrollPointY = frame.size.height - documentVisibleRect.size.height
scrollPoint.y = min(maxScrollPointY, scrollPoint.y)
let clipView = scrollView.contentView
let rClipView = NSMakeRect(scrollPoint.x, scrollPoint.y, NSWidth(clipView.bounds), NSHeight(clipView.bounds))
clipView.animator().bounds = rClipView
}
func visibleRowViews() -> [TimelineTableRowView]? {
guard let scrollView = self.enclosingScrollView, numberOfRows > 0 else {
return nil
}
let range = rows(in: scrollView.documentVisibleRect)
let ixMax = numberOfRows - 1
let ixStart = min(range.location, ixMax)
let ixEnd = min(((range.location + range.length) - 1), ixMax)
var visibleRows = [TimelineTableRowView]()
for ixRow in ixStart...ixEnd {
if let oneRowView = rowView(atRow: ixRow, makeIfNecessary: false) as? TimelineTableRowView {
visibleRows += [oneRowView]
}
}
return visibleRows.isEmpty ? nil : visibleRows
}
func redrawGrid() {
visibleRowViews()?.forEach { $0.invalidateGridRect() }
}
}

View File

@ -0,0 +1,182 @@
//
// PreferencesWindowController.swift
// Evergreen
//
// Created by Brent Simmons on 8/1/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Cocoa
private struct PreferencesToolbarItemSpec {
let identifier: String // Toolbar item identifier and view controller identifier in storyboard
let name: String
let imageName: String
}
private let toolbarItemIdentifierGeneral = "General"
class PreferencesWindowController : NSWindowController, NSToolbarDelegate {
private let windowFrameName = "Preferences"
fileprivate var viewControllers = [String: NSViewController]()
fileprivate let toolbarItemSpecs: [PreferencesToolbarItemSpec] = {
var specs = [PreferencesToolbarItemSpec]()
specs += [PreferencesToolbarItemSpec(identifier: toolbarItemIdentifierGeneral, name: NSLocalizedString("General", comment: "Preferences"), imageName: NSImageNamePreferencesGeneral)]
return specs
}()
override func windowDidLoad() {
let toolbar = NSToolbar(identifier: "PreferencesToolbar")
toolbar.delegate = self
toolbar.autosavesConfiguration = false
toolbar.allowsUserCustomization = false
toolbar.displayMode = .iconAndLabel
toolbar.selectedItemIdentifier = toolbarItemSpecs.first!.identifier
window?.showsToolbarButton = false
window?.toolbar = toolbar
window?.setFrameAutosaveName(windowFrameName)
switchToViewAtIndex(0)
}
// MARK: Actions
func toolbarItemClicked(_ sender: AnyObject) {
}
// MARK: NSToolbarDelegate
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: String, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
guard let toolbarItemSpec = toolbarItemSpecs.first(where: { $0.identifier == itemIdentifier }) else {
return nil
}
let toolbarItem = NSToolbarItem(itemIdentifier: toolbarItemSpec.identifier)
toolbarItem.action = #selector(toolbarItemClicked(_:))
toolbarItem.target = self
toolbarItem.label = toolbarItemSpec.name
toolbarItem.paletteLabel = toolbarItem.label
toolbarItem.image = NSImage(named: toolbarItemSpec.imageName)
return toolbarItem
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
return toolbarItemSpecs.map { $0.identifier }
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
return toolbarDefaultItemIdentifiers(toolbar)
}
func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
return toolbarDefaultItemIdentifiers(toolbar)
}
}
private extension PreferencesWindowController {
var currentView: NSView? {
get {
return window?.contentView?.subviews.first
}
}
func toolbarItemSpec(for identifier: String) -> PreferencesToolbarItemSpec? {
return toolbarItemSpecs.first(where: { $0.identifier == identifier })
}
func switchToViewAtIndex(_ index: Int) {
let identifier = toolbarItemSpecs[index].identifier
switchToView(identifier: identifier)
}
func switchToView(identifier: String) {
guard let toolbarItemSpec = toolbarItemSpec(for: identifier) else {
assertionFailure("Preferences window: no toolbarItemSpec matching \(identifier).")
return
}
guard let newViewController = viewController(identifier: identifier) else {
assertionFailure("Preferences window: no view controller matching \(identifier).")
return
}
if newViewController.view == currentView {
return
}
newViewController.view.nextResponder = newViewController
newViewController.nextResponder = window!.contentView
window!.title = toolbarItemSpec.name
resizeWindow(toFitView: newViewController.view)
if let currentView = currentView {
window!.contentView?.replaceSubview(currentView, with: newViewController.view)
}
else {
window!.contentView?.addSubview(newViewController.view)
}
window!.makeFirstResponder(newViewController.view)
}
func viewController(identifier: String) -> NSViewController? {
if let cachedViewController = viewControllers[identifier] {
return cachedViewController
}
let storyboard = NSStoryboard(name: "Preferences", bundle: nil)
guard let viewController = storyboard.instantiateController(withIdentifier: identifier) as? NSViewController else {
assertionFailure("Unknown preferences view controller: \(identifier)")
return nil
}
viewControllers[identifier] = viewController
return viewController
}
func resizeWindow(toFitView view: NSView) {
let viewFrame = view.frame
let windowFrame = window!.frame
let contentViewFrame = window!.contentView!.frame
let deltaHeight = NSHeight(contentViewFrame) - NSHeight(viewFrame)
let heightForWindow = NSHeight(windowFrame) - deltaHeight
let windowOriginY = NSMinY(windowFrame)// + deltaHeight
var updatedWindowFrame = windowFrame
updatedWindowFrame.size.height = heightForWindow
updatedWindowFrame.origin.y = windowOriginY
updatedWindowFrame.size.width = NSWidth(viewFrame)
var updatedViewFrame = viewFrame
updatedViewFrame.origin = NSZeroPoint
if viewFrame != updatedViewFrame {
view.frame = updatedViewFrame
}
if windowFrame != updatedWindowFrame {
window!.setFrame(updatedWindowFrame, display: true, animate: true)
}
}
}

View File

@ -0,0 +1,38 @@
//
// IndeterminateProgressWindowController.swift
// Evergreen
//
// Created by Brent Simmons on 8/28/16.
// Copyright © 2016 Ranchero Software. All rights reserved.
//
import Cocoa
class IndeterminateProgressWindowController: NSWindowController {
@IBOutlet var messageLabel: NSTextField!
@IBOutlet var progressIndicator: NSProgressIndicator!
dynamic var message = ""
convenience init(message: String) {
self.init(windowNibName: "IndeterminateProgressWindow")
self.message = message
}
override func windowDidLoad() {
progressIndicator.startAnimation(self)
}
}
func runIndeterminateProgressWithMessage(_ message: String) {
let windowController = IndeterminateProgressWindowController(message: message)
NSApplication.shared().runModal(for: windowController.window!)
}
func stopIndeterminateProgress() {
NSApplication.shared().stopModal()
}

View File

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>note</key>
<string>By Brent Simmons, Evergreen developer</string>
<key>editedName</key>
<string>Inessential</string>
<key>home</key>
<string>http://inessential.com/</string>
<key>url</key>
<string>http://inessential.com/xml/rss.xml</string>
</dict>
<dict>
<key>home</key>
<string>http://daringfireball.net/</string>
<key>editedName</key>
<string>Daring Fireball</string>
<key>url</key>
<string>http://daringfireball.net/feeds/main</string>
</dict>
<dict>
<key>home</key>
<string>https://www.natashatherobot.com/</string>
<key>editedName</key>
<string>Natasha the Robot</string>
<key>url</key>
<string>https://www.natashatherobot.com/feed/</string>
</dict>
<dict>
<key>home</key>
<string>http://www.imore.com/</string>
<key>editedName</key>
<string>iMore</string>
<key>url</key>
<string>http://www.imore.com/rss.xml</string>
</dict>
<dict>
<key>home</key>
<string>http://becky.coffee/</string>
<key>note</key>
<string>iOS developer &amp; corgi enthusiast</string>
<key>editedName</key>
<string>Becky Hansmeyer</string>
<key>url</key>
<string>http://beckyhansmeyer.com/feed/</string>
</dict>
<dict>
<key>home</key>
<string>http://shapeof.com/</string>
<key>note</key>
<string>By Gus Mueller, Acorn developer</string>
<key>editedName</key>
<string>The Shape of Everything</string>
<key>url</key>
<string>http://shapeof.com/rss.xml</string>
</dict>
<dict>
<key>home</key>
<string>http://redqueencoder.com/</string>
<key>note</key>
<string>Janie Clayton</string>
<key>editedName</key>
<string>The Red Queen Coder</string>
<key>url</key>
<string>http://redqueencoder.com/feed/</string>
</dict>
<dict>
<key>home</key>
<string>https://medium.com/@jaimeejaimee</string>
<key>editedName</key>
<string>jaimeejaimee</string>
<key>note</key>
<string>Jaimee Newberry</string>
<key>url</key>
<string>https://medium.com/feed/@jaimeejaimee</string>
</dict>
<dict>
<key>home</key>
<string>http://sixcolors.com</string>
<key>note</key>
<string>By Jason Snell &amp; friends</string>
<key>editedName</key>
<string>Six Colors</string>
<key>url</key>
<string>http://feedpress.me/sixcolors</string>
</dict>
<dict>
<key>home</key>
<string>http://www.loopinsight.com/</string>
<key>note</key>
<string>Jim Dalrymple and Dave Mark</string>
<key>editedName</key>
<string>Loop Insight</string>
<key>url</key>
<string>http://www.loopinsight.com/feed/</string>
</dict>
<dict>
<key>home</key>
<string>http://katiefloyd.com/</string>
<key>editedName</key>
<string>Katie Floyd</string>
<key>url</key>
<string>http://feed.katiefloyd.com/</string>
</dict>
<dict>
<key>home</key>
<string>http://ashleynh.me/</string>
<key>editedName</key>
<string>Ashley Nelson-Hornstein</string>
<key>url</key>
<string>http://blog.ashleynh.me/rss/</string>
</dict>
<dict>
<key>home</key>
<string>http://ranchero.com/evergreen/</string>
<key>editedName</key>
<string>Evergreen News</string>
<key>url</key>
<string>http://ranchero.com/evergreen/feed.json</string>
</dict>
<dict>
<key>home</key>
<string>http://scripting.com/</string>
<key>editedName</key>
<string>Scripting News</string>
<key>note</key>
<string>Dave Winer</string>
<key>url</key>
<string>http://scripting.com/rss.xml</string>
</dict>
<dict>
<key>home</key>
<string>http://ericasadun.com/</string>
<key>editedName</key>
<string>Erica Sadun</string>
<key>note</key>
<string>Lots of Swift goodness.</string>
<key>url</key>
<string>http://ericasadun.com/feed/</string>
</dict>
<dict>
<key>home</key>
<string>http://onefoottsunami.com/</string>
<key>editedName</key>
<string>One Foot Tsunami</string>
<key>note</key>
<string>Paul Kafasis</string>
<key>url</key>
<string>http://onefoottsunami.com/feed/atom/</string>
</dict>
</array>
</plist>

View File

@ -0,0 +1,86 @@
body {
color: #444;
background-color: white;
margin-top: 42px;
margin-bottom: 100px;
margin-left: 80px;
margin-right: 80px;
font-family: -apple-system;
font-size: 18px;
}
a {
text-decoration: none;
}
a, a:link, a:visited {
color: #015cdc;
}
a:hover {
text-decoration: underline;
}
#articleDateline {
color: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
border-top: 1px solid rgba(0, 0, 0, 0.1);
padding-bottom: 6px;
padding-top: 6px;
margin-bottom: 25px;
}
#articleDateline a:link, #articleDateline a:visited {
color: rgba(0, 0, 0, 0.4);
}
#articleDescription {
line-height: 1.5em;
}
h1 {
line-height: 1.15em;
font-weight: medium;
}
h1, h2, h3, h4, h5, h6 {
font-family: -apple-system, "Helvetica Neue"
}
code, pre {
font-family: "SF Mono", Menlo, "Courier New", Courier, monospace;
font-size: 14px;
color: #666;
}
pre {
white-space: pre-wrap;
}
img {
max-width: 100%;
}
/*Block ads and junk*/
iframe[src*="feedads"],
iframe[src*="doubleclick"],
iframe[src*="plusone.google"] {
display: none !important;
}
a[href*=".ads."],
a[href*="feedads"],
a[href*="feedburner"],
a[href*="doubleclick"],
a[href*="//ads."],
a[href*="api.tweetmeme"],
a[href*="delicious.com/post?"],
a[href*="digg.com/submit?"],
a[href*="google.com/bookmarks/mark?"],
a[href*="posterous.com/share?"],
a[href*="tumblr.com/share?"],
a[href*="linkedin.com/shareArticle?"],
a[href*="facebook.com/share.php?"],
a[href*="http://twitter.com/home?"],
a[href*="addtoany.com/share_save"] {
display: none !important;
}
img[src*=".ads."],
img[src*="//ads."],
img[src*="doubleclick"],
img[src*="feedads"],
img[src*="feedburner"],
img[src*="share-buttons"] {
display: none !important;
}

View File

@ -0,0 +1,3 @@
<div id="articleTitle"><h1>[[newsitem_title]]</h1></div>
<div id="articleDateline">[[feedlink_withfavicon]] • <span class="articleDate">[[date_medium]]</span></div>
<div id="articleDescription">[[newsitem_description]]</div>

View File

@ -1,6 +1,6 @@
// //
// AccountProtocol.swift // AccountProtocol.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/17/16. // Created by Brent Simmons on 4/17/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// ArticleProtocol.swift // ArticleProtocol.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/23/16. // Created by Brent Simmons on 4/23/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// ArticleStatusProtocol.swift // ArticleStatusProtocol.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/23/16. // Created by Brent Simmons on 4/23/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// FeedProtocol.swift // FeedProtocol.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/23/16. // Created by Brent Simmons on 4/23/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// FolderProtocol.swift // FolderProtocol.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/17/16. // Created by Brent Simmons on 4/17/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// UnreadCountProtocol.swift // UnreadCountProtocol.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/8/16. // Created by Brent Simmons on 4/8/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// DiskDictionaryConstants.swift // DiskDictionaryConstants.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/9/16. // Created by Brent Simmons on 4/9/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// LocalAccount.swift // LocalAccount.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/23/16. // Created by Brent Simmons on 4/23/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// LocalArticle.swift // LocalArticle.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/23/16. // Created by Brent Simmons on 4/23/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
@ -25,7 +25,7 @@ public final class LocalArticle: NSObject, Article {
get { get {
if _body == nil, let d = bodyData { if _body == nil, let d = bodyData {
// print(title) // print(title)
if let s = NSString(data: d, encoding: String.Encoding.utf8.rawValue) as? String { if let s = NSString(data: d, encoding: String.Encoding.utf8.rawValue) as String? {
_body = s _body = s
bodyData = nil bodyData = nil
} }

View File

@ -1,6 +1,6 @@
// //
// LocalArticleCache.swift // LocalArticleCache.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 5/9/16. // Created by Brent Simmons on 5/9/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// LocalArticleStatus.swift // LocalArticleStatus.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/23/16. // Created by Brent Simmons on 4/23/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// LocalDatabase.swift // LocalDatabase.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 7/20/15. // Created by Brent Simmons on 7/20/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved. // Copyright © 2015 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// LocalFeed.swift // LocalFeed.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/23/16. // Created by Brent Simmons on 4/23/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// LocalFolder.swift // LocalFolder.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 4/23/16. // Created by Brent Simmons on 4/23/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// LocalStatusesManager.swift // LocalStatusesManager.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 5/8/16. // Created by Brent Simmons on 5/8/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.

View File

@ -71,6 +71,10 @@ typedef struct {
+ (NSString *)rs_stringWithNumberOfTabs:(NSInteger)numberOfTabs; + (NSString *)rs_stringWithNumberOfTabs:(NSInteger)numberOfTabs;
- (NSString *)rs_stringByPrependingNumberOfTabs:(NSInteger)numberOfTabs; - (NSString *)rs_stringByPrependingNumberOfTabs:(NSInteger)numberOfTabs;
// Remove leading http:// or https://
- (NSString *)rs_stringByStrippingHTTPOrHTTPSScheme;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@ -366,5 +366,13 @@ NSString *RSStringReplaceAll(NSString *stringToSearch, NSString *searchFor, NSSt
} }
- (NSString *)rs_stringByStrippingHTTPOrHTTPSScheme {
NSString *s = [self rs_stringByStrippingPrefix:@"http://" caseSensitive:NO];
s = [s rs_stringByStrippingPrefix:@"https://" caseSensitive:NO];
return s;
}
@end @end

View File

@ -1,6 +1,6 @@
// //
// Node.swift // Node.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 7/21/15. // Created by Brent Simmons on 7/21/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved. // Copyright © 2015 Ranchero Software, LLC. All rights reserved.

View File

@ -1,6 +1,6 @@
// //
// TreeController.swift // TreeController.swift
// Rainier // Evergreen
// //
// Created by Brent Simmons on 5/29/16. // Created by Brent Simmons on 5/29/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved. // Copyright © 2016 Ranchero Software, LLC. All rights reserved.