From 44461af07a5ed6affb9a6d71f15a2634a6d7c8d2 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 4 Nov 2017 12:19:34 -0700 Subject: [PATCH] Read feed directory data from disk. --- Evergreen.xcodeproj/project.pbxproj | 12 +++ Evergreen/FeedList/FeedList.plist | 8 +- Evergreen/FeedList/FeedListFeed.swift | 46 +++++++++ Evergreen/FeedList/FeedListFolder.swift | 28 ++++++ .../FeedListTreeControllerDelegate.swift | 93 +++++++++++++++++++ .../FeedList/FeedListViewController.swift | 36 ++++++- 6 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 Evergreen/FeedList/FeedListFeed.swift create mode 100644 Evergreen/FeedList/FeedListFolder.swift create mode 100644 Evergreen/FeedList/FeedListTreeControllerDelegate.swift diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 542de2252..e0861025a 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -79,6 +79,9 @@ 84B06FFE1ED3818D00F0B54B /* RSTree.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FFA1ED3818000F0B54B /* RSTree.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 84B0700A1ED3822600F0B54B /* RSTextDrawing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B070071ED3821900F0B54B /* RSTextDrawing.framework */; }; 84B0700B1ED3822600F0B54B /* RSTextDrawing.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B070071ED3821900F0B54B /* RSTextDrawing.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 84B99C671FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C661FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift */; }; + 84B99C691FAE36B800ECDEDB /* FeedListFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C681FAE36B800ECDEDB /* FeedListFolder.swift */; }; + 84B99C6B1FAE370B00ECDEDB /* FeedListFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C6A1FAE370B00ECDEDB /* FeedListFeed.swift */; }; 84BB4B771F11753300858766 /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84BB4B681F1174D400858766 /* Data.framework */; }; 84BB4B781F11753300858766 /* Data.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84BB4B681F1174D400858766 /* Data.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 84DAEE301F86CAFE0058304B /* OPMLImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */; }; @@ -446,6 +449,9 @@ 84B06FE01ED3803200F0B54B /* RSFeedFinder.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSFeedFinder.xcodeproj; path = Frameworks/RSFeedFinder/RSFeedFinder.xcodeproj; sourceTree = ""; }; 84B06FF41ED3818000F0B54B /* RSTree.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSTree.xcodeproj; path = Frameworks/RSTree/RSTree.xcodeproj; sourceTree = ""; }; 84B070011ED3821800F0B54B /* RSTextDrawing.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSTextDrawing.xcodeproj; path = Frameworks/RSTextDrawing/RSTextDrawing.xcodeproj; sourceTree = ""; }; + 84B99C661FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListTreeControllerDelegate.swift; sourceTree = ""; }; + 84B99C681FAE36B800ECDEDB /* FeedListFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListFolder.swift; sourceTree = ""; }; + 84B99C6A1FAE370B00ECDEDB /* FeedListFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListFeed.swift; sourceTree = ""; }; 84BB4B611F1174D400858766 /* Data.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Data.xcodeproj; path = Frameworks/Data/Data.xcodeproj; sourceTree = ""; }; 84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLImporter.swift; sourceTree = ""; }; 84DAEE311F870B390058304B /* DockBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DockBadge.swift; path = Evergreen/DockBadge.swift; sourceTree = ""; }; @@ -644,6 +650,9 @@ 84F204CD1FAACB660076E152 /* FeedListViewController.swift */, 84F204DD1FAACB8B0076E152 /* FeedListTimelineViewController.swift */, 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */, + 84B99C661FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift */, + 84B99C681FAE36B800ECDEDB /* FeedListFolder.swift */, + 84B99C6A1FAE370B00ECDEDB /* FeedListFeed.swift */, 84E95CF61FABB3C800552D99 /* FeedList.plist */, ); name = "Feed List"; @@ -1224,11 +1233,14 @@ 849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */, 849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */, 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */, + 84B99C6B1FAE370B00ECDEDB /* FeedListFeed.swift in Sources */, 849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */, 849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */, 849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */, 849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */, 849A976D1ED9EBC8007D329B /* TimelineTableView.swift in Sources */, + 84B99C671FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift in Sources */, + 84B99C691FAE36B800ECDEDB /* FeedListFolder.swift in Sources */, 84F204DE1FAACB8B0076E152 /* FeedListTimelineViewController.swift in Sources */, 849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */, 849A97851ED9ECCD007D329B /* PreferencesWindowController.swift in Sources */, diff --git a/Evergreen/FeedList/FeedList.plist b/Evergreen/FeedList/FeedList.plist index f3c7bf1c4..3bfe2d56d 100644 --- a/Evergreen/FeedList/FeedList.plist +++ b/Evergreen/FeedList/FeedList.plist @@ -5,7 +5,7 @@ Mac and iOS - editedName + name TidBITS homePageURL http://tidbits.org/ @@ -13,7 +13,7 @@ http://tidbits.org/feeds/tidbits_blurb.rss - editedName + name Michael Tsai homePageURL https://mjtsai.com/blog/ @@ -24,7 +24,7 @@ Blogs - editedName + name Jason Kottke homePageURL https://kottke.org/ @@ -35,7 +35,7 @@ News - editedName + name Talking Points Memo homePageURL https://talkingpointsmemo.com/ diff --git a/Evergreen/FeedList/FeedListFeed.swift b/Evergreen/FeedList/FeedListFeed.swift new file mode 100644 index 000000000..cde60f337 --- /dev/null +++ b/Evergreen/FeedList/FeedListFeed.swift @@ -0,0 +1,46 @@ +// +// FeedListFeed.swift +// Evergreen +// +// Created by Brent Simmons on 11/4/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation + +final class FeedListFeed: Hashable { + + let name: String + let url: String + let homePageURL: String + let hashValue: Int + + init(name: String, url: String, homePageURL: String) { + + self.name = name + self.url = url + self.homePageURL = homePageURL + self.hashValue = url.hashValue + } + + private struct Key { + static let name = "name" + static let editedName = "editedName" // Used in DefaultFeeds.plist + static let url = "url" + static let homePageURL = "homePageURL" + } + + convenience init(dictionary: [String: String]) { + + let name = (dictionary[Key.name] ?? dictionary[Key.editedName])! + let url = dictionary[Key.url]! + let homePageURL = dictionary[Key.homePageURL]! + + self.init(name: name, url: url, homePageURL: homePageURL) + } + + static func ==(lhs: FeedListFeed, rhs: FeedListFeed) -> Bool { + + return lhs.hashValue == rhs.hashValue && lhs.url == rhs.url && lhs.name == rhs.name && lhs.homePageURL == rhs.homePageURL + } +} diff --git a/Evergreen/FeedList/FeedListFolder.swift b/Evergreen/FeedList/FeedListFolder.swift new file mode 100644 index 000000000..8883707a3 --- /dev/null +++ b/Evergreen/FeedList/FeedListFolder.swift @@ -0,0 +1,28 @@ +// +// FeedListFolder.swift +// Evergreen +// +// Created by Brent Simmons on 11/4/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation + +final class FeedListFolder: Hashable { + + let name: String + let feeds: Set + let hashValue: Int + + init(name: String, feeds: Set) { + + self.name = name + self.feeds = feeds + self.hashValue = name.hashValue + } + + static func ==(lhs: FeedListFolder, rhs: FeedListFolder) -> Bool { + + return lhs === rhs + } +} diff --git a/Evergreen/FeedList/FeedListTreeControllerDelegate.swift b/Evergreen/FeedList/FeedListTreeControllerDelegate.swift new file mode 100644 index 000000000..59bdc4a1e --- /dev/null +++ b/Evergreen/FeedList/FeedListTreeControllerDelegate.swift @@ -0,0 +1,93 @@ +// +// FeedListTreeControllerDelegate.swift +// Evergreen +// +// Created by Brent Simmons on 11/4/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSTree + +// Folders and feeds that appear in the Feed Directory are pulled from three sources: +// 1. Feeds added in code here. (Evergreen News should be the only one.) +// 2. Default feeds for new users — see DefaultFeeds.plist. +// 3. FeedList.plist — the main directory. Its top level is all folders. There are no sub-folders. +// It’s okay if there’s overlap: a feed may appear in multiple places. +// If there’s any problem with the data (wrong types), this will crash. By design. + +final class FeedListTreeControllerDelegate: TreeControllerDelegate { + + let topLevelFeeds: Set + let defaultFeeds: Set + let folders: Set + + init() { + + let evergreenNewsFeed = FeedListFeed(name: "Evergreen News", url: "https://ranchero.com/evergreen/feed.json", homePageURL: "https://ranchero.com/evergreen/blog/") + self.topLevelFeeds = Set([evergreenNewsFeed]) + + self.defaultFeeds = FeedListReader.defaultFeeds() + self.folders = FeedListReader.folders() + } + + func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { + +// if node.isRoot { +// return childNodesForRootNode(node) +// } +// if node.representedObject is FeedListFolder { +// return childNodesForFolderNode(node) +// } + + return nil + } +} + + + +//private extension FeedListTreeControllerDelegate { +// +// func childNodesForRootNode(_ rootNode: Node) -> [Node]? { +// +// return childNodesForContainerNode(rootNode, AccountManager.shared.localAccount.children) +// } +// +//} + +// MARK: - Loading from Disk + +private struct FeedListReader { + + static func folders() -> Set { + + return Set(foldersDictionary().map { (arg: (key: String, value: [[String : String]])) -> FeedListFolder in + + let (name, feedDictionaries) = arg + return FeedListFolder(name: name, feeds: feeds(with: feedDictionaries)) + }) + } + + static func defaultFeeds() -> Set { + + return feeds(with: defaultFeedDictionaries()) + } + + private static func defaultFeedDictionaries() -> [[String: String]] { + + let f = Bundle.main.path(forResource: "DefaultFeeds", ofType: "plist")! + return NSArray(contentsOfFile: f)! as! [[String: String]] + } + + private static func foldersDictionary() -> [String: [[String: String]]] { + + let f = Bundle.main.path(forResource: "FeedList", ofType: "plist")! + return NSDictionary(contentsOfFile: f)! as! [String: [[String: String]]] + } + + private static func feeds(with dictionaries: [[String: String]]) -> Set { + + return Set(dictionaries.map { FeedListFeed(dictionary: $0) }) + } +} + diff --git a/Evergreen/FeedList/FeedListViewController.swift b/Evergreen/FeedList/FeedListViewController.swift index 8cbe9ac0c..cc4cf5b91 100644 --- a/Evergreen/FeedList/FeedListViewController.swift +++ b/Evergreen/FeedList/FeedListViewController.swift @@ -7,8 +7,42 @@ // import Cocoa +import RSTree final class FeedListViewController: NSViewController { - + @IBOutlet var outlineView: NSOutlineView! + private let treeControllerDelegate = FeedListTreeControllerDelegate() + lazy var treeController: TreeController = { + TreeController(delegate: treeControllerDelegate) + }() + +} + +// MARK: - NSOutlineViewDataSource + +extension FeedListViewController: 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 + } + + private func nodeForItem(_ item: AnyObject?) -> Node { + + if item == nil { + return treeController.rootNode + } + return item as! Node + } }