├── images ├── app-store-badge.png └── Feed-Curator-Banner.png ├── FeedCurator ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ ├── icon_16x16.png │ │ ├── icon_32x32.png │ │ ├── icon_128x128.png │ │ ├── icon_256x256.png │ │ ├── icon_512x512.png │ │ ├── icon_16x16@2x@2x.png │ │ ├── icon_32x32@2x@2x.png │ │ ├── icon_128x128@2x@2x.png │ │ ├── icon_256x256@2x@2x.png │ │ ├── icon_512x512@2x@2x.png │ │ └── Contents.json ├── FeedCurator.entitlements ├── OutlineView.swift ├── Extensions │ └── String+.swift ├── Sheets │ ├── IndeterminateProgress.swift │ ├── UpdateTitle.swift │ ├── AddFeed.swift │ ├── IndeterminateProgress.xib │ ├── UpdateTitle.xib │ └── AddFeed.xib ├── FeedFinder │ ├── InitialFeedDownloader.swift │ ├── HTMLFeedFinder.swift │ ├── FeedSpecifier.swift │ └── FeedFinder.swift ├── Model │ ├── RSOPMLItem+.swift │ ├── OPMLDocument.swift │ ├── OPMLFeed.swift │ └── OPMLEntry.swift ├── AppDefaults.swift ├── WindowController.swift ├── AppDelegate.swift ├── Credits.rtf ├── Info.plist ├── Document.swift ├── ViewController+OutlineView.swift ├── ViewController.swift └── Base.lproj │ └── Main.storyboard ├── cleanPrefsAndData ├── Feed Curator.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── maurice.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── .gitmodules ├── Feed Curator.entitlements ├── LICENSE.md ├── .gitignore └── README.md /images/app-store-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/images/app-store-badge.png -------------------------------------------------------------------------------- /images/Feed-Curator-Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/images/Feed-Curator-Banner.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /cleanPrefsAndData: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | defaults delete io.vincode.FeedCurator 4 | killall cfprefsd 5 | 6 | rm -rf ~/Library/Containers/io.vincode.FeedCurator 7 | -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincode-io/FeedCurator/HEAD/FeedCurator/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /FeedCurator/FeedCurator.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Feed Curator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Feed Curator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/RSParser"] 2 | path = submodules/RSParser 3 | url = https://github.com/vincode-io/RSParser.git 4 | [submodule "submodules/RSCore"] 5 | path = submodules/RSCore 6 | url = https://github.com/vincode-io/RSCore.git 7 | [submodule "submodules/RSWeb"] 8 | path = submodules/RSWeb 9 | url = https://github.com/vincode-io/RSWeb.git 10 | -------------------------------------------------------------------------------- /FeedCurator/OutlineView.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import AppKit 4 | 5 | class OutlineView: NSOutlineView { 6 | 7 | override func frameOfCell(atColumn column: Int, row: Int) -> NSRect { 8 | var frame = super.frameOfCell(atColumn: column, row: row) 9 | frame.origin.x += 4.0 10 | frame.size.width -= 4.0 11 | return frame 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /FeedCurator/Extensions/String+.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import Foundation 4 | 5 | // This allows Strings to be directly thrown as errors. It is a quick and easy way to do 6 | // what is essentially an untyped error with a message. 7 | extension String: LocalizedError { 8 | public var localizedDescription: String { 9 | return self 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Feed Curator.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /FeedCurator/Sheets/IndeterminateProgress.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import AppKit 4 | 5 | class IndeterminateProgress: NSWindowController { 6 | 7 | @IBOutlet weak var messageLabel: NSTextField! 8 | @IBOutlet weak var progressIndicator: NSProgressIndicator! 9 | 10 | private var message: String! 11 | 12 | convenience init(message: String) { 13 | self.init(windowNibName: NSNib.Name("IndeterminateProgress")) 14 | self.message = message 15 | } 16 | 17 | override func windowDidLoad() { 18 | super.windowDidLoad() 19 | messageLabel.stringValue = message 20 | progressIndicator.startAnimation(self) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Feed Curator.xcodeproj/xcuserdata/maurice.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Feed Curator.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | FeedCurator.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 518C65A222443C7B00B18604 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /FeedCurator/FeedFinder/InitialFeedDownloader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InitialFeedDownloader.swift 3 | // NetNewsWire 4 | // 5 | // Created by Brent Simmons on 9/3/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RSParser 11 | import RSWeb 12 | 13 | struct InitialFeedDownloader { 14 | 15 | static func download(_ url: URL,_ completionHandler: @escaping (_ parsedFeed: ParsedFeed?) -> Void) { 16 | 17 | downloadUsingCache(url) { (data, response, error) in 18 | 19 | guard let data = data else { 20 | completionHandler(nil) 21 | return 22 | } 23 | 24 | let parserData = ParserData(url: url.absoluteString, data: data) 25 | FeedParser.parse(parserData) { (parsedFeed, error) in 26 | completionHandler(parsedFeed) 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /FeedCurator/Model/RSOPMLItem+.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import Foundation 4 | import RSParser 5 | 6 | extension RSOPMLItem { 7 | 8 | func translateToOPMLEntry(parent: OPMLEntry?) -> OPMLEntry { 9 | 10 | let opmlEntry: OPMLEntry = { 11 | if let fs = self.feedSpecifier { 12 | return OPMLFeed(title: titleFromAttributes, pageURL: fs.homePageURL, feedURL: fs.feedURL, parent: parent) 13 | } else { 14 | if let document = self as? RSOPMLDocument { 15 | return OPMLDocument(title: document.title) 16 | } else { 17 | return OPMLEntry(title: titleFromAttributes, parent: parent) 18 | } 19 | } 20 | }() 21 | 22 | if let opmlItems = children { 23 | for opmlItem in opmlItems { 24 | let childEntry = opmlItem.translateToOPMLEntry(parent: opmlEntry) 25 | opmlEntry.entries.append(childEntry) 26 | } 27 | } 28 | 29 | return opmlEntry 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /FeedCurator/Model/OPMLDocument.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import Foundation 4 | 5 | class OPMLDocument: OPMLEntry { 6 | 7 | var isValid: Bool { 8 | return title != nil && !entries.isEmpty 9 | } 10 | 11 | override func makeXML(indentLevel: Int) -> String { 12 | 13 | var s = 14 | """ 15 | 16 | 17 | 18 | 19 | \(title ?? "") 20 | 21 | 22 | 23 | """ 24 | 25 | for entry in entries { 26 | s += entry.makeXML(indentLevel: indentLevel + 1) 27 | } 28 | 29 | s += 30 | """ 31 | 32 | 33 | """ 34 | 35 | return s 36 | 37 | } 38 | 39 | func entry(for address: OPMLEntryAddress) -> OPMLEntry { 40 | var result: OPMLEntry = self 41 | for i in address { 42 | result = result.entries[i] 43 | } 44 | return result 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /FeedCurator/AppDefaults.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import Foundation 4 | 5 | struct AppDefaults { 6 | 7 | struct Key { 8 | static let gistIds = "gistIds" 9 | static let gistRawURLs = "gistRawURLs" 10 | static let issueURLs = "issueURLs" 11 | } 12 | 13 | static var gistIds: [String: Any?]? { 14 | get { 15 | return UserDefaults.standard.dictionary(forKey: Key.gistIds) 16 | } 17 | set { 18 | UserDefaults.standard.set(newValue, forKey: Key.gistIds) 19 | } 20 | } 21 | 22 | static var gistRawURLs: [String: Any?]? { 23 | get { 24 | return UserDefaults.standard.dictionary(forKey: Key.gistRawURLs) 25 | } 26 | set { 27 | UserDefaults.standard.set(newValue, forKey: Key.gistRawURLs) 28 | } 29 | } 30 | 31 | static var issueURLs: [String: Any?]? { 32 | get { 33 | return UserDefaults.standard.dictionary(forKey: Key.issueURLs) 34 | } 35 | set { 36 | UserDefaults.standard.set(newValue, forKey: Key.issueURLs) 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /FeedCurator/WindowController.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import AppKit 4 | 5 | class WindowController: NSWindowController { 6 | 7 | static let clickHere = NSLocalizedString("Click Here to Add Title", comment: "Area to click to add title") 8 | 9 | private var updateTitle: UpdateTitle? 10 | 11 | @IBOutlet weak var titleButton: NSButton! 12 | 13 | override func windowDidLoad() { 14 | super.windowDidLoad() 15 | self.shouldCascadeWindows = true 16 | NotificationCenter.default.addObserver(self, selector: #selector(opmlDocumentTitleDidChange(_:)), name: .OPMLDocumentTitleDidChange, object: nil) 17 | } 18 | 19 | @IBAction func titleButtonClicked(_ sender: NSButton) { 20 | updateTitle = UpdateTitle() 21 | updateTitle!.runSheetOnWindow(window!) 22 | } 23 | 24 | @objc func opmlDocumentTitleDidChange(_ note: Notification) { 25 | if let doc = self.document as? Document { 26 | if doc.opmlDocument.title == nil { 27 | titleButton.title = WindowController.clickHere 28 | } else { 29 | titleButton.title = doc.opmlDocument.title ?? "" 30 | } 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vincode, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /FeedCurator/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import AppKit 4 | import RSWeb 5 | 6 | var appDelegate: AppDelegate! 7 | 8 | @NSApplicationMain 9 | class AppDelegate: NSObject, NSApplicationDelegate { 10 | 11 | override init() { 12 | super.init() 13 | appDelegate = self 14 | } 15 | 16 | func applicationWillFinishLaunching(_ notification: Notification) { 17 | } 18 | 19 | func applicationDidFinishLaunching(_ notification: Notification) { 20 | } 21 | 22 | @IBAction func showHelp(_ sender: Any?) { 23 | MacWebBrowser.openURL(URL(string: "https://vincode.io/feed-curator-help/")!, inBackground: false) 24 | } 25 | 26 | @IBAction func showWebsite(_ sender: Any?) { 27 | MacWebBrowser.openURL(URL(string: "https://vincode.io/feed-curator/")!, inBackground: false) 28 | } 29 | 30 | @IBAction func showGithubRepo(_ sender: Any?) { 31 | MacWebBrowser.openURL(URL(string: "https://github.com/vincode-io/FeedCurator")!, inBackground: false) 32 | } 33 | 34 | @IBAction func showGithubIssues(_ sender: Any?) { 35 | MacWebBrowser.openURL(URL(string: "https://github.com/vincode-io/FeedCurator/issues")!, inBackground: false) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /FeedCurator/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2708 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande-Bold;\f1\fnil\fcharset0 LucidaGrande;} 3 | {\colortbl;\red255\green255\blue255;\red0\green0\blue0;} 4 | {\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;} 5 | {\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid1\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}} 6 | {\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} 7 | \vieww14060\viewh15660\viewkind0 8 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\sl360\slmult1\pardirnatural\partightenfactor0 9 | 10 | \f0\b\fs22 \cf2 By Maurice C. Parker\ 11 | {\field{\*\fldinst{HYPERLINK "https://vincode.io"}}{\fldrslt vincode.io}} 12 | \f1\b0 \ 13 | \pard\pardeftab720\li360\sa60\partightenfactor0 14 | \cf2 \ 15 | \pard\pardeftab720\sa60\partightenfactor0 16 | 17 | \f0\b \cf2 Credits: 18 | \f1\b0 \ 19 | \pard\tx220\tx720\pardeftab720\li720\fi-720\sa60\partightenfactor0 20 | \ls1\ilvl0\cf2 {\listtext \uc0\u8226 }I'd like to thank {\field{\*\fldinst{HYPERLINK "http://inessential.com"}}{\fldrslt Brent Simmons}} for open sourcing {\field{\*\fldinst{HYPERLINK "https://ranchero.com/netnewswire/"}}{\fldrslt NetNewsWire}}. There is a lot of NetNewsWire DNA in this app.} -------------------------------------------------------------------------------- /FeedCurator/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /FeedCurator/Sheets/UpdateTitle.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import AppKit 4 | 5 | class UpdateTitle: NSWindowController { 6 | 7 | @IBOutlet weak var titleTextField: NSTextField! 8 | 9 | private weak var hostWindow: NSWindow? 10 | 11 | convenience init() { 12 | self.init(windowNibName: NSNib.Name("UpdateTitle")) 13 | } 14 | 15 | override func windowDidLoad() { 16 | 17 | guard let windowController = hostWindow?.windowController as? WindowController else { 18 | assertionFailure() 19 | return 20 | } 21 | 22 | if windowController.titleButton.title != WindowController.clickHere { 23 | titleTextField.stringValue = windowController.titleButton.title 24 | } 25 | 26 | } 27 | 28 | func runSheetOnWindow(_ hostWindow: NSWindow) { 29 | 30 | self.hostWindow = hostWindow 31 | 32 | hostWindow.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in 33 | if returnCode == NSApplication.ModalResponse.OK { 34 | self.updateTitle() 35 | } 36 | } 37 | 38 | } 39 | 40 | private func updateTitle() { 41 | 42 | // Update the model 43 | guard let document = hostWindow?.windowController?.document as? Document else { 44 | assertionFailure() 45 | return 46 | } 47 | 48 | if titleTextField.stringValue.isEmpty { 49 | document.updateTitle(entry: document.opmlDocument, title: nil) 50 | } else { 51 | document.updateTitle(entry: document.opmlDocument, title: titleTextField.stringValue) 52 | } 53 | 54 | } 55 | 56 | @IBAction func cancel(_ sender: NSButton) { 57 | hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) 58 | } 59 | 60 | @IBAction func update(_ sender: NSButton) { 61 | hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /FeedCurator/Sheets/AddFeed.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import AppKit 4 | 5 | protocol AddFeedDelegate: class { 6 | func addFeedUserDidAdd(_ : String) 7 | func addFeedUserDidCancel() 8 | } 9 | 10 | class AddFeed: NSWindowController { 11 | 12 | @IBOutlet weak var urlTextField: NSTextField! 13 | 14 | @IBOutlet weak var progressIndicator: NSProgressIndicator! 15 | @IBOutlet weak var cancelButton: NSButton! 16 | @IBOutlet weak var addButton: NSButton! 17 | 18 | private weak var hostWindow: NSWindow? 19 | private weak var delegate: AddFeedDelegate? 20 | 21 | convenience init(delegate: AddFeedDelegate) { 22 | self.init(windowNibName: NSNib.Name("AddFeed")) 23 | self.delegate = delegate 24 | } 25 | 26 | func runSheetOnWindow(_ hostWindow: NSWindow) { 27 | 28 | self.hostWindow = hostWindow 29 | 30 | hostWindow.beginSheet(window!) { [unowned self] (returnCode: NSApplication.ModalResponse) -> Void in 31 | if returnCode == NSApplication.ModalResponse.OK { 32 | self.delegate?.addFeedUserDidAdd(self.urlTextField.stringValue) 33 | } else { 34 | self.delegate?.addFeedUserDidCancel() 35 | } 36 | } 37 | 38 | } 39 | 40 | // MARK: Actions 41 | 42 | @IBAction func cancel(_ sender: NSButton) { 43 | hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) 44 | } 45 | 46 | @IBAction func add(_ sender: NSButton) { 47 | hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK) 48 | } 49 | 50 | // MARK: NSTextFieldDelegate 51 | 52 | @objc func controlTextDidEndEditing(_ obj: Notification) { 53 | updateUI() 54 | } 55 | 56 | @objc func controlTextDidChange(_ obj: Notification) { 57 | updateUI() 58 | } 59 | 60 | private func updateUI() { 61 | addButton.isEnabled = urlTextField.stringValue.rs_stringMayBeURL() 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | .build/ 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | # Pods/ 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots 71 | fastlane/test_output 72 | 73 | # Project specific 74 | 75 | Configuration/Release.xcconfig 76 | Feed\ Curator.xcodeproj/xcshareddata 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Feed Curator Icon](images/Feed-Curator-Banner.png) 2 | 3 | # 4 | 5 | Feed Curator is a macOS application that makes it easier to OPML listings 6 | of blog feeds. It is a companion app to 7 | [Feed Compass](https://github.com/vincode-io/FeedCompass). You can use 8 | Feed Curator to create, publish, and maintain the blog listings that 9 | users of Feed Compass see. It can also be used as a standalone OPML feed editor. 10 | 11 | ## Download 12 | 13 | [![App Store](images/app-store-badge.png)](https://itunes.apple.com/us/app/feed-curator/id1458647758) 14 | 15 | ## Features 16 | 17 | - Create and edit OPML feed files 18 | - Feed Finder - Just drag a page URL to Feed Curator and it will find the 19 | feed for you 20 | - Automatic population of feed details: Title, URL, and Home Page 21 | - Drag and drop rearrangement of feeds 22 | - Free publishing of OPML files using Github Gist 23 | - Easy submission to Feed Compass for inclusion in Feed Compass 24 | 25 | ## Feedback 26 | 27 | Have something you want to say about Feed Curator? Leave a comment in our 28 | [Feedback](https://github.com/vincode-io/FeedCurator/issues/1) issue. 29 | 30 | ## Contributing 31 | 32 | Pull requests are welcome. If you are a non-technical person and want to 33 | contribute you can file bug reports and feature requests on the 34 | [Issues](https://github.com/vincode-io/FeedCurator/issues) page. 35 | 36 | ## Building 37 | 38 | From the command line run the following commands: 39 | ``` 40 | git clone https://github.com/vincode-io/FeedCurator.git 41 | cd FeedCurator 42 | git submodule init 43 | git submodule update 44 | ``` 45 | 46 | You can now open the Feed Curator.xcodeproj project. You will have to fix 47 | the project signing before building and running. 48 | 49 | ## Licence 50 | 51 | MIT Licensed 52 | 53 | ## Credits 54 | 55 | Many thanks to [Brent Simmons](http://inessential.com) releasing 56 | [NetNewsWire](https://github.com/brentsimmons/NetNewsWire) as an Open 57 | Source project. There is a lot of NetNewsWire DNA in Feed Curator. 58 | -------------------------------------------------------------------------------- /FeedCurator/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeExtensions 11 | 12 | opml 13 | xml 14 | 15 | CFBundleTypeIconFile 16 | 17 | CFBundleTypeName 18 | OPML 19 | CFBundleTypeOSTypes 20 | 21 | ???? 22 | 23 | CFBundleTypeRole 24 | Editor 25 | LSHandlerRank 26 | Default 27 | NSDocumentClass 28 | $(PRODUCT_MODULE_NAME).Document 29 | 30 | 31 | CFBundleExecutable 32 | $(EXECUTABLE_NAME) 33 | CFBundleIconFile 34 | 35 | CFBundleIdentifier 36 | $(PRODUCT_BUNDLE_IDENTIFIER) 37 | CFBundleInfoDictionaryVersion 38 | 6.0 39 | CFBundleName 40 | $(PRODUCT_NAME) 41 | CFBundlePackageType 42 | APPL 43 | CFBundleShortVersionString 44 | $(MARKETING_VERSION) 45 | CFBundleURLTypes 46 | 47 | 48 | CFBundleTypeRole 49 | Viewer 50 | CFBundleURLName 51 | Feed Curator 52 | CFBundleURLSchemes 53 | 54 | feedcurator 55 | 56 | 57 | 58 | CFBundleVersion 59 | 897 60 | LSApplicationCategoryType 61 | public.app-category.social-networking 62 | LSMinimumSystemVersion 63 | $(MACOSX_DEPLOYMENT_TARGET) 64 | NSAppTransportSecurity 65 | 66 | NSAllowsArbitraryLoads 67 | 68 | 69 | NSHumanReadableCopyright 70 | Copyright © 2019-2023 Vincode, Inc. All rights reserved. 71 | NSMainStoryboardFile 72 | Main 73 | NSPrincipalClass 74 | NSApplication 75 | 76 | 77 | -------------------------------------------------------------------------------- /FeedCurator/FeedFinder/HTMLFeedFinder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLFeedFinder.swift 3 | // FeedFinder 4 | // 5 | // Created by Brent Simmons on 8/7/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RSParser 11 | 12 | private let feedURLWordsToMatch = ["feed", "xml", "rss", "atom", "json"] 13 | 14 | class HTMLFeedFinder { 15 | 16 | var feedSpecifiers: Set { 17 | return Set(feedSpecifiersDictionary.values) 18 | } 19 | 20 | private var feedSpecifiersDictionary = [String: FeedSpecifier]() 21 | 22 | init(parserData: ParserData) { 23 | 24 | let metadata = RSHTMLMetadataParser.htmlMetadata(with: parserData) 25 | 26 | for oneFeedLink in metadata.feedLinks { 27 | if let oneURLString = oneFeedLink.urlString { 28 | let oneFeedSpecifier = FeedSpecifier(title: oneFeedLink.title, urlString: oneURLString, source: .HTMLHead) 29 | addFeedSpecifier(oneFeedSpecifier) 30 | } 31 | } 32 | 33 | if let bodyLinks = RSHTMLLinkParser.htmlLinks(with: parserData) { 34 | for oneBodyLink in bodyLinks { 35 | 36 | if linkMightBeFeed(oneBodyLink) { 37 | let normalizedURL = oneBodyLink.urlString.rs_normalizedURL() 38 | let oneFeedSpecifier = FeedSpecifier(title: oneBodyLink.text, urlString: normalizedURL, source: .HTMLLink) 39 | addFeedSpecifier(oneFeedSpecifier) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | private extension HTMLFeedFinder { 47 | 48 | func addFeedSpecifier(_ feedSpecifier: FeedSpecifier) { 49 | 50 | // If there’s an existing feed specifier, merge the two so that we have the best data. If one has a title and one doesn’t, use that non-nil title. Use the better source. 51 | 52 | if let existingFeedSpecifier = feedSpecifiersDictionary[feedSpecifier.urlString] { 53 | let mergedFeedSpecifier = existingFeedSpecifier.feedSpecifierByMerging(feedSpecifier) 54 | feedSpecifiersDictionary[feedSpecifier.urlString] = mergedFeedSpecifier 55 | } 56 | else { 57 | feedSpecifiersDictionary[feedSpecifier.urlString] = feedSpecifier 58 | } 59 | } 60 | 61 | func urlStringMightBeFeed(_ urlString: String) -> Bool { 62 | 63 | let massagedURLString = urlString.replacingOccurrences(of: "buzzfeed", with: "_") 64 | 65 | for oneMatch in feedURLWordsToMatch { 66 | let range = (massagedURLString as NSString).range(of: oneMatch, options: .caseInsensitive) 67 | if range.length > 0 { 68 | return true 69 | } 70 | } 71 | 72 | return false 73 | } 74 | 75 | func linkMightBeFeed(_ link: RSHTMLLink) -> Bool { 76 | 77 | if let linkURLString = link.urlString, urlStringMightBeFeed(linkURLString) { 78 | return true 79 | } 80 | return false 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /FeedCurator/FeedFinder/FeedSpecifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedSpecifier.swift 3 | // FeedFinder 4 | // 5 | // Created by Brent Simmons on 8/7/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct FeedSpecifier: Hashable { 12 | 13 | enum Source: Int { 14 | 15 | case UserEntered = 0, HTMLHead, HTMLLink 16 | 17 | func equalToOrBetterThan(_ otherSource: Source) -> Bool { 18 | 19 | return self.rawValue <= otherSource.rawValue 20 | } 21 | } 22 | 23 | public let title: String? 24 | public let urlString: String 25 | public let source: Source 26 | public var score: Int { 27 | return calculatedScore() 28 | } 29 | 30 | func feedSpecifierByMerging(_ feedSpecifier: FeedSpecifier) -> FeedSpecifier { 31 | 32 | // Take the best data (non-nil title, better source) to create a new feed specifier; 33 | 34 | let mergedTitle = title ?? feedSpecifier.title 35 | let mergedSource = source.equalToOrBetterThan(feedSpecifier.source) ? source : feedSpecifier.source 36 | 37 | return FeedSpecifier(title: mergedTitle, urlString: urlString, source: mergedSource) 38 | } 39 | 40 | public static func bestFeed(in feedSpecifiers: Set) -> FeedSpecifier? { 41 | 42 | if feedSpecifiers.isEmpty { 43 | return nil 44 | } 45 | if feedSpecifiers.count == 1 { 46 | return feedSpecifiers.anyObject() 47 | } 48 | 49 | var currentHighScore = 0 50 | var currentBestFeed: FeedSpecifier? = nil 51 | 52 | for oneFeedSpecifier in feedSpecifiers { 53 | let oneScore = oneFeedSpecifier.score 54 | if oneScore > currentHighScore { 55 | currentHighScore = oneScore 56 | currentBestFeed = oneFeedSpecifier 57 | } 58 | } 59 | 60 | return currentBestFeed 61 | } 62 | } 63 | 64 | private extension FeedSpecifier { 65 | 66 | func calculatedScore() -> Int { 67 | 68 | var score = 0 69 | 70 | if source == .UserEntered { 71 | return 1000 72 | } 73 | else if source == .HTMLHead { 74 | score = score + 50 75 | } 76 | 77 | if urlString.rs_caseInsensitiveContains("comments") { 78 | score = score - 10 79 | } 80 | if urlString.rs_caseInsensitiveContains("rss") { 81 | score = score + 5 82 | } 83 | if urlString.hasSuffix("/feed/") { 84 | score = score + 5 85 | } 86 | if urlString.hasSuffix("/feed") { 87 | score = score + 4 88 | } 89 | if urlString.rs_caseInsensitiveContains("json") { 90 | score = score + 6 91 | } 92 | 93 | if let title = title { 94 | if title.rs_caseInsensitiveContains("comments") { 95 | score = score - 10 96 | } 97 | if title.rs_caseInsensitiveContains("json") { 98 | score = score + 1 99 | } 100 | } 101 | 102 | return score 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /FeedCurator/Model/OPMLFeed.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import Foundation 4 | import RSCore 5 | 6 | class OPMLFeed: OPMLEntry { 7 | 8 | static let feedUTI = "io.vincode.opml-feed" 9 | static let feedUTIType = NSPasteboard.PasteboardType(rawValue: feedUTI) 10 | 11 | struct Key { 12 | static let pageURL = "pageURL" 13 | static let feedURL = "feedURL" 14 | } 15 | 16 | var pageURL: String? 17 | var feedURL: String? 18 | 19 | init(feedURL: String) { 20 | super.init(title: nil) 21 | self.feedURL = feedURL 22 | } 23 | 24 | init(title: String?, pageURL: String?, feedURL: String?, parent: OPMLEntry? = nil) { 25 | super.init(title: title, parent: parent) 26 | self.pageURL = pageURL 27 | self.feedURL = feedURL 28 | } 29 | 30 | convenience init?(plist: [String: Any]) { 31 | let title = plist[OPMLEntry.Key.title] as? String 32 | let pageURL = plist[Key.pageURL] as? String 33 | let feedURL = plist[Key.feedURL] as? String 34 | self.init(title: title, pageURL: pageURL, feedURL: feedURL) 35 | overrideAddress = plist[OPMLEntry.Key.address] as? OPMLEntryAddress 36 | } 37 | 38 | convenience init?(pasteboardItem: NSPasteboardItem) { 39 | 40 | if pasteboardItem.types.contains(OPMLFeed.feedUTIType) { 41 | guard let plist = pasteboardItem.propertyList(forType: OPMLFeed.feedUTIType) as? [String: Any] else { 42 | return nil 43 | } 44 | self.init(plist: plist) 45 | return 46 | } 47 | 48 | var pasteboardType: NSPasteboard.PasteboardType? 49 | if pasteboardItem.types.contains(.URL) { 50 | pasteboardType = .URL 51 | } 52 | else if pasteboardItem.types.contains(.string) { 53 | pasteboardType = .string 54 | } 55 | if let foundType = pasteboardType { 56 | if let possibleURLString = pasteboardItem.string(forType: foundType) { 57 | if possibleURLString.rs_stringMayBeURL() { 58 | self.init(feedURL: possibleURLString) 59 | return 60 | } 61 | } 62 | } 63 | 64 | return nil 65 | 66 | } 67 | 68 | override func makeXML(indentLevel: Int) -> String { 69 | 70 | let t = title?.rs_stringByEscapingSpecialXMLCharacters() ?? "" 71 | let p = pageURL?.rs_stringByEscapingSpecialXMLCharacters() ?? "" 72 | let f = feedURL?.rs_stringByEscapingSpecialXMLCharacters() ?? "" 73 | 74 | var s = "\n" 75 | s = s.rs_string(byPrependingNumberOfTabs: indentLevel) 76 | 77 | return s 78 | 79 | } 80 | 81 | override func makePlist() -> Any? { 82 | 83 | var result = [String: Any]() 84 | 85 | result[OPMLEntry.Key.uti] = OPMLFeed.feedUTI 86 | result[OPMLEntry.Key.address] = address 87 | result[OPMLEntry.Key.title] = title 88 | result[Key.pageURL] = pageURL 89 | result[Key.feedURL] = feedURL 90 | 91 | return result 92 | 93 | } 94 | 95 | // MARK: NSPasteboardWriting 96 | 97 | override func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] { 98 | return [OPMLFeed.feedUTIType, .URL, .string] 99 | } 100 | 101 | override func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { 102 | 103 | let plist: Any? 104 | 105 | switch type { 106 | case .string: 107 | plist = title 108 | case .URL: 109 | plist = feedURL 110 | case OPMLFeed.feedUTIType: 111 | plist = makePlist() 112 | default: 113 | plist = nil 114 | } 115 | 116 | return plist 117 | 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /FeedCurator/Model/OPMLEntry.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import Foundation 4 | import RSCore 5 | 6 | typealias OPMLEntryAddress = [Int] 7 | 8 | class OPMLEntry: NSObject, NSPasteboardWriting { 9 | 10 | static let folderUTI = "io.vincode.opml-folder" 11 | static let folderUTIType = NSPasteboard.PasteboardType(rawValue: folderUTI) 12 | 13 | struct Key { 14 | static let uti = "uti" 15 | static let address = "address" 16 | static let title = "title" 17 | static let entries = "entries" 18 | } 19 | 20 | var overrideAddress: OPMLEntryAddress? 21 | var title: String? 22 | var entries = [OPMLEntry]() 23 | 24 | weak var parent: OPMLEntry? { 25 | didSet { 26 | overrideAddress = nil 27 | } 28 | } 29 | 30 | var address: OPMLEntryAddress { 31 | if overrideAddress != nil { 32 | return overrideAddress! 33 | } 34 | if parent != nil { 35 | let backwardsAddress = parent!.mapAddress(child: self, workAddress: OPMLEntryAddress()) 36 | return backwardsAddress.reversed() 37 | } 38 | return [] 39 | } 40 | 41 | var isFolder: Bool { 42 | return type(of: self) == OPMLEntry.self 43 | } 44 | 45 | init(title: String?, parent: OPMLEntry? = nil) { 46 | super.init() 47 | self.title = title 48 | self.parent = parent 49 | } 50 | 51 | convenience init?(plist: [String: Any]) { 52 | let title = plist[Key.title] as? String 53 | self.init(title: title) 54 | overrideAddress = plist[Key.address] as? OPMLEntryAddress 55 | } 56 | 57 | convenience init?(pasteboardItem: NSPasteboardItem) { 58 | guard let plist = pasteboardItem.propertyList(forType: OPMLEntry.folderUTIType) as? [String: Any] else { 59 | return nil 60 | } 61 | self.init(plist: plist) 62 | } 63 | 64 | func makeXML(indentLevel: Int) -> String { 65 | 66 | let t = title?.rs_stringByEscapingSpecialXMLCharacters() ?? "" 67 | var s = "\n".rs_string(byPrependingNumberOfTabs: indentLevel) 68 | 69 | for entry in entries { 70 | s += entry.makeXML(indentLevel: indentLevel + 1) 71 | } 72 | 73 | s += "\n".rs_string(byPrependingNumberOfTabs: indentLevel) 74 | 75 | return s 76 | 77 | } 78 | 79 | // MARK: Drag and Drop 80 | 81 | func makePlist() -> Any? { 82 | var result = [String: Any]() 83 | result[Key.uti] = OPMLEntry.folderUTI 84 | result[Key.address] = address 85 | result[Key.title] = title 86 | return result 87 | } 88 | 89 | static func entries(with pasteboard: NSPasteboard) -> [OPMLEntry]? { 90 | 91 | guard let items = pasteboard.pasteboardItems else { 92 | return nil 93 | } 94 | 95 | let results: [OPMLEntry] = items.compactMap { item in 96 | if item.types.contains(OPMLEntry.folderUTIType) { 97 | return OPMLEntry(pasteboardItem: item) 98 | } else { 99 | return OPMLFeed(pasteboardItem: item) 100 | } 101 | } 102 | 103 | return results.isEmpty ? nil : results 104 | 105 | } 106 | 107 | static func entry(plist: [String: Any]) -> OPMLEntry? { 108 | if let uti = plist[OPMLFeed.feedUTI] as? String { 109 | if uti == OPMLFeed.feedUTI { 110 | return OPMLFeed(plist: plist) 111 | } else { 112 | return OPMLEntry(plist: plist) 113 | } 114 | } 115 | return nil 116 | } 117 | 118 | // MARK: NSPasteboardWriting 119 | 120 | func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] { 121 | return [OPMLEntry.folderUTIType, .string] 122 | } 123 | 124 | func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { 125 | let plist: Any? 126 | 127 | switch type { 128 | case .string: 129 | plist = title 130 | case OPMLEntry.folderUTIType: 131 | plist = makePlist() 132 | default: 133 | plist = nil 134 | } 135 | 136 | return plist 137 | } 138 | 139 | static func == (lhs: OPMLEntry, rhs: OPMLEntry) -> Bool { 140 | return lhs.address == rhs.address 141 | } 142 | 143 | } 144 | 145 | private extension OPMLEntry { 146 | 147 | func mapAddress(child: OPMLEntry, workAddress: OPMLEntryAddress) -> OPMLEntryAddress { 148 | 149 | var workAddress = workAddress 150 | workAddress.append(entries.firstIndex(of: child)!) 151 | 152 | if let parent = parent { 153 | return parent.mapAddress(child: self, workAddress: workAddress) 154 | } else { 155 | return workAddress 156 | } 157 | 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /FeedCurator/Sheets/IndeterminateProgress.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /FeedCurator/Document.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import AppKit 4 | import RSParser 5 | 6 | public extension Notification.Name { 7 | static let OPMLDocumentTitleDidChange = Notification.Name(rawValue: "OPMLDocumentTitleDidChange") 8 | static let OPMLDocumentChildrenDidChange = Notification.Name(rawValue: "OPMLDocumentChildrenDidChange") 9 | } 10 | 11 | class Document: NSDocument { 12 | 13 | var opmlDocument = OPMLDocument(title: nil) 14 | 15 | var filename: String? { 16 | return fileURL?.absoluteURL.lastPathComponent 17 | } 18 | 19 | var uploadedURL: String? { 20 | if let filename = filename { 21 | return AppDefaults.gistRawURLs?[filename] as? String 22 | } else { 23 | return nil 24 | } 25 | } 26 | 27 | var issueURL: String? { 28 | if let filename = filename { 29 | return AppDefaults.issueURLs?[filename] as? String 30 | } else { 31 | return nil 32 | } 33 | } 34 | 35 | override class var autosavesInPlace: Bool { 36 | return true 37 | } 38 | 39 | override func makeWindowControllers() { 40 | // Returns the Storyboard that contains your Document window. 41 | let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) 42 | let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController 43 | self.addWindowController(windowController) 44 | } 45 | 46 | override func data(ofType typeName: String) throws -> Data { 47 | 48 | let xml = opmlDocument.makeXML(indentLevel: 0) 49 | let xmlData = xml.data(using: .utf8) 50 | 51 | if xmlData == nil || xmlData!.count < 1 { 52 | throw NSLocalizedString("Error generating OPML file", comment: "Error generating OPML file") 53 | } 54 | 55 | return xmlData! 56 | 57 | } 58 | 59 | override func read(from data: Data, ofType typeName: String) throws { 60 | let parserData = ParserData(url: "", data: data) 61 | let rsDoc = try RSOPMLParser.parseOPML(with: parserData) 62 | opmlDocument = rsDoc.translateToOPMLEntry(parent: nil) as! OPMLDocument 63 | } 64 | 65 | override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { 66 | 67 | if item.action == #selector(save(_:)) { 68 | if opmlDocument.entries.isEmpty { 69 | return false 70 | } 71 | } 72 | 73 | return super.validateUserInterfaceItem(item) 74 | 75 | } 76 | 77 | func updateTitle(entry: OPMLEntry, title: String?) { 78 | 79 | let oldTitle = entry.title 80 | 81 | if entry is OPMLDocument { 82 | undoManager?.setActionName(NSLocalizedString("Title Change", comment: "Update Title")) 83 | } else { 84 | undoManager?.setActionName(NSLocalizedString("Folder Rename", comment: "Update Title")) 85 | } 86 | 87 | undoManager?.registerUndo(withTarget: opmlDocument) { [weak self] target in 88 | self?.updateTitle(entry: entry, title: oldTitle) 89 | } 90 | 91 | entry.title = title 92 | 93 | // Not too happy with this. Revisit and refactor later. 94 | if entry is OPMLDocument { 95 | NotificationCenter.default.post(name: .OPMLDocumentTitleDidChange, object: self, userInfo: nil) 96 | } else { 97 | NotificationCenter.default.post(name: .OPMLDocumentChildrenDidChange, object: self, userInfo: nil) 98 | } 99 | 100 | } 101 | 102 | func removeEntry(parent: OPMLEntry, childIndex: Int) { 103 | 104 | let current = parent.entries[childIndex] 105 | 106 | if !(undoManager?.isUndoing ?? false) { 107 | if current.isFolder { 108 | undoManager?.setActionName(NSLocalizedString("Delete Folder", comment: "Delete Folder")) 109 | } else { 110 | undoManager?.setActionName(NSLocalizedString("Delete Feed", comment: "Delete Row")) 111 | } 112 | } 113 | 114 | undoManager?.registerUndo(withTarget: parent) { [weak self] target in 115 | self?.insertEntry(parent: target, childIndex: childIndex, entry: current) 116 | NotificationCenter.default.post(name: .OPMLDocumentChildrenDidChange, object: self, userInfo: nil) 117 | } 118 | 119 | parent.entries.remove(at: childIndex) 120 | 121 | } 122 | 123 | func insertEntry(parent: OPMLEntry, childIndex: Int, entry: OPMLEntry) { 124 | 125 | if !(undoManager?.isUndoing ?? false) { 126 | if entry.isFolder { 127 | undoManager?.setActionName(NSLocalizedString("Insert Folder", comment: "Insert Folder")) 128 | } else { 129 | undoManager?.setActionName(NSLocalizedString("Insert Feed", comment: "Insert Row")) 130 | } 131 | } 132 | 133 | undoManager?.registerUndo(withTarget: parent) { [weak self] target in 134 | self?.removeEntry(parent: target, childIndex: childIndex) 135 | NotificationCenter.default.post(name: .OPMLDocumentChildrenDidChange, object: self, userInfo: nil) 136 | } 137 | 138 | parent.entries.insert(entry, at: childIndex) 139 | entry.parent = parent 140 | 141 | } 142 | 143 | func moveEntry(fromParent: OPMLEntry, fromChildIndex: Int, toParent: OPMLEntry, toChildIndex: Int, entry: OPMLEntry) { 144 | 145 | if !(undoManager?.isUndoing ?? false) { 146 | if entry.isFolder { 147 | undoManager?.setActionName(NSLocalizedString("Move Folder", comment: "Insert Folder")) 148 | } else { 149 | undoManager?.setActionName(NSLocalizedString("Move Feed", comment: "Insert Row")) 150 | } 151 | } 152 | 153 | undoManager?.registerUndo(withTarget: fromParent) { [weak self] target in 154 | self?.moveEntry(fromParent: toParent, fromChildIndex: toChildIndex, toParent: fromParent, toChildIndex: fromChildIndex, entry: entry) 155 | NotificationCenter.default.post(name: .OPMLDocumentChildrenDidChange, object: self, userInfo: nil) 156 | } 157 | 158 | fromParent.entries.remove(at: fromChildIndex) 159 | toParent.entries.insert(entry, at: toChildIndex) 160 | entry.parent = toParent 161 | 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /FeedCurator/ViewController+OutlineView.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import AppKit 4 | import RSCore 5 | 6 | // MARK: NSOutlineViewDataSource 7 | 8 | extension ViewController: NSOutlineViewDataSource { 9 | 10 | func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { 11 | 12 | if item == nil { 13 | return document!.opmlDocument.entries[index] 14 | } 15 | 16 | let entry = item as! OPMLEntry 17 | return entry.entries[index] 18 | 19 | } 20 | 21 | func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { 22 | 23 | if item == nil { 24 | return document?.opmlDocument.entries.count ?? 0 25 | } 26 | 27 | let entry = item as! OPMLEntry 28 | return entry.entries.count 29 | 30 | } 31 | 32 | func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { 33 | let entry = item as! OPMLEntry 34 | return entry.isFolder 35 | } 36 | 37 | } 38 | 39 | // MARK: NSOutlineViewDelegate 40 | 41 | extension ViewController: NSOutlineViewDelegate { 42 | 43 | func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { 44 | 45 | switch tableColumn?.identifier.rawValue { 46 | case "nameColumn": 47 | if let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "nameCell"), owner: nil) as? NSTableCellView { 48 | let entry = item as! OPMLEntry 49 | if entry.isFolder { 50 | cell.imageView?.image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil) 51 | cell.textField?.isEditable = true 52 | } else { 53 | cell.imageView?.image = NSImage(systemSymbolName: "globe", accessibilityDescription: nil)?.withSymbolConfiguration(.init(pointSize: 14, weight: .light)) 54 | cell.textField?.isEditable = false 55 | } 56 | cell.imageView?.contentTintColor = NSColor.controlAccentColor 57 | cell.textField?.stringValue = entry.title ?? "" 58 | return cell 59 | } 60 | case "pageColumn": 61 | if let feed = item as? OPMLFeed { 62 | if let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "pageCell"), owner: nil) as? NSTableCellView { 63 | cell.textField?.stringValue = feed.pageURL ?? "" 64 | return cell 65 | } 66 | } 67 | case "feedColumn": 68 | if let feed = item as? OPMLFeed { 69 | if let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "feedCell"), owner: nil) as? NSTableCellView { 70 | cell.textField?.stringValue = feed.feedURL ?? "" 71 | return cell 72 | } 73 | } 74 | default: 75 | return nil 76 | } 77 | 78 | return nil 79 | 80 | } 81 | 82 | func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { 83 | return 22.0 84 | } 85 | 86 | func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool { 87 | return false 88 | } 89 | 90 | func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool { 91 | let entry = item as! OPMLEntry 92 | return entry.isFolder 93 | } 94 | 95 | func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool { 96 | return true 97 | } 98 | 99 | func outlineView(_ outlineView: NSOutlineView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItems draggedItems: [Any]) { 100 | } 101 | 102 | // Drag and Drop 103 | 104 | func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { 105 | return item as? NSPasteboardWriting 106 | } 107 | 108 | func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { 109 | guard let draggedEntries = OPMLEntry.entries(with: info.draggingPasteboard), !draggedEntries.isEmpty else { 110 | return [] 111 | } 112 | 113 | guard !(item is OPMLFeed) else { 114 | return [] 115 | } 116 | 117 | if let proposedEntry = item as? OPMLEntry, proposedEntry.address == draggedEntries.first?.address { 118 | return [] 119 | } 120 | 121 | if (info.draggingSource as AnyObject) === outlineView { 122 | return .move 123 | } else { 124 | if draggedEntries.first?.isFolder ?? true { 125 | return [] 126 | } else { 127 | return .copy 128 | } 129 | } 130 | } 131 | 132 | func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { 133 | guard let draggedEntries = OPMLEntry.entries(with: info.draggingPasteboard), !draggedEntries.isEmpty else { 134 | return false 135 | } 136 | 137 | if (info.draggingSource as AnyObject) === outlineView { 138 | acceptLocalDrop(outlineView, draggedEntries, parent: item as? OPMLEntry, index) 139 | } else { 140 | acceptNonLocalDrop(outlineView, draggedEntries, parent: item as? OPMLEntry, index) 141 | } 142 | 143 | return true 144 | } 145 | 146 | } 147 | 148 | private extension ViewController { 149 | 150 | func acceptNonLocalDrop(_ outlineView: NSOutlineView, _ draggedEntries: [OPMLEntry], parent: OPMLEntry?, _ index: Int) { 151 | for entry in draggedEntries { 152 | if let feed = entry as? OPMLFeed, let feedURL = feed.feedURL { 153 | currentDragData = (parent: parent, index: index) 154 | findFeed(feedURL) 155 | } 156 | } 157 | } 158 | 159 | func acceptLocalDrop(_ outlineView: NSOutlineView, _ draggedEntries: [OPMLEntry], parent: OPMLEntry?, _ index: Int) { 160 | for foundEntries in draggedEntries.compactMap({ $0.address }).compactMap({ document?.opmlDocument.entry(for: $0) }) { 161 | moveEntry(foundEntries, toParent: parent, toChildIndex: index) 162 | } 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /FeedCurator/Sheets/UpdateTitle.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 49 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /FeedCurator/FeedFinder/FeedFinder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedFinder.swift 3 | // FeedFinder 4 | // 5 | // Created by Brent Simmons on 8/2/16. 6 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RSParser 11 | import RSWeb 12 | import RSCore 13 | 14 | protocol FeedFinderDelegate: class { 15 | 16 | func feedFinder(_: FeedFinder, didFindFeeds: Set) 17 | } 18 | 19 | class FeedFinder { 20 | 21 | private weak var delegate: FeedFinderDelegate? 22 | private var feedSpecifiers = [String: FeedSpecifier]() 23 | private var didNotifyDelegate = false 24 | 25 | var initialDownloadError: Error? 26 | var initialDownloadStatusCode = -1 27 | 28 | init(url: URL, delegate: FeedFinderDelegate) { 29 | 30 | self.delegate = delegate 31 | 32 | DispatchQueue.main.async() { () -> Void in 33 | 34 | self.findFeeds(url) 35 | } 36 | } 37 | 38 | deinit { 39 | notifyDelegateIfNeeded() 40 | } 41 | } 42 | 43 | private extension FeedFinder { 44 | 45 | func addFeedSpecifier(_ feedSpecifier: FeedSpecifier) { 46 | 47 | // If there’s an existing feed specifier, merge the two so that we have the best data. If one has a title and one doesn’t, use that non-nil title. Use the better source. 48 | 49 | if let existingFeedSpecifier = feedSpecifiers[feedSpecifier.urlString] { 50 | let mergedFeedSpecifier = existingFeedSpecifier.feedSpecifierByMerging(feedSpecifier) 51 | feedSpecifiers[feedSpecifier.urlString] = mergedFeedSpecifier 52 | } 53 | else { 54 | feedSpecifiers[feedSpecifier.urlString] = feedSpecifier 55 | } 56 | } 57 | 58 | func findFeedsInHTMLPage(htmlData: Data, urlString: String) { 59 | 60 | // Feeds in the section we automatically assume are feeds. 61 | // If there are none from the section, 62 | // then possible feeds in section are downloaded individually 63 | // and added once we determine they are feeds. 64 | 65 | let possibleFeedSpecifiers = possibleFeedsInHTMLPage(htmlData: htmlData, urlString: urlString) 66 | var feedSpecifiersToDownload = Set() 67 | 68 | var didFindFeedInHTMLHead = false 69 | 70 | for oneFeedSpecifier in possibleFeedSpecifiers { 71 | if oneFeedSpecifier.source == .HTMLHead { 72 | addFeedSpecifier(oneFeedSpecifier) 73 | didFindFeedInHTMLHead = true 74 | } 75 | else { 76 | if !feedSpecifiersContainsURLString(oneFeedSpecifier.urlString) { 77 | feedSpecifiersToDownload.insert(oneFeedSpecifier) 78 | } 79 | } 80 | } 81 | 82 | if didFindFeedInHTMLHead || feedSpecifiersToDownload.isEmpty { 83 | stopFinding() 84 | } 85 | else { 86 | downloadFeedSpecifiers(feedSpecifiersToDownload) 87 | } 88 | } 89 | 90 | func possibleFeedsInHTMLPage(htmlData: Data, urlString: String) -> Set { 91 | 92 | let parserData = ParserData(url: urlString, data: htmlData) 93 | var feedSpecifiers = HTMLFeedFinder(parserData: parserData).feedSpecifiers 94 | 95 | if feedSpecifiers.isEmpty { 96 | // Odds are decent it’s a WordPress site, and just adding /feed/ will work. 97 | // It’s also fairly common for /index.xml to work. 98 | if let url = URL(string: urlString) { 99 | let feedURL = url.appendingPathComponent("feed", isDirectory: true) 100 | let wordpressFeedSpecifier = FeedSpecifier(title: nil, urlString: feedURL.absoluteString, source: .HTMLLink) 101 | feedSpecifiers.insert(wordpressFeedSpecifier) 102 | 103 | let indexXMLURL = url.appendingPathComponent("index.xml", isDirectory: false) 104 | let indexXMLFeedSpecifier = FeedSpecifier(title: nil, urlString: indexXMLURL.absoluteString, source: .HTMLLink) 105 | feedSpecifiers.insert(indexXMLFeedSpecifier) 106 | } 107 | } 108 | 109 | return feedSpecifiers 110 | } 111 | 112 | func feedSpecifiersContainsURLString(_ urlString: String) -> Bool { 113 | 114 | if let _ = feedSpecifiers[urlString] { 115 | return true 116 | } 117 | return false 118 | } 119 | 120 | func isHTML(_ data: Data) -> Bool { 121 | 122 | return (data as NSData).rs_dataIsProbablyHTML() 123 | } 124 | 125 | func findFeeds(_ initialURL: URL) { 126 | 127 | downloadInitialFeed(initialURL) 128 | } 129 | 130 | func downloadInitialFeed(_ initialURL: URL) { 131 | 132 | downloadUsingCache(initialURL) { (data, response, error) in 133 | 134 | self.initialDownloadStatusCode = response?.forcedStatusCode ?? -1 135 | 136 | if let error = error { 137 | self.initialDownloadError = error 138 | self.stopFinding() 139 | return 140 | } 141 | guard let data = data, let response = response else { 142 | self.stopFinding() 143 | return 144 | } 145 | 146 | if !response.statusIsOK || data.isEmpty { 147 | self.stopFinding() 148 | return 149 | } 150 | 151 | if self.isFeed(data, initialURL.absoluteString) { 152 | let feedSpecifier = FeedSpecifier(title: nil, urlString: initialURL.absoluteString, source: .UserEntered) 153 | self.addFeedSpecifier(feedSpecifier) 154 | self.stopFinding() 155 | return 156 | } 157 | 158 | if !self.isHTML(data) { 159 | self.stopFinding() 160 | return 161 | } 162 | 163 | self.findFeedsInHTMLPage(htmlData: data, urlString: initialURL.absoluteString) 164 | } 165 | } 166 | 167 | func downloadFeedSpecifiers(_ feedSpecifiers: Set) { 168 | 169 | var pendingDownloads = feedSpecifiers 170 | 171 | for oneFeedSpecifier in feedSpecifiers { 172 | 173 | guard let url = URL(string: oneFeedSpecifier.urlString) else { 174 | pendingDownloads.remove(oneFeedSpecifier) 175 | continue 176 | } 177 | 178 | downloadUsingCache(url) { (data, response, error) in 179 | 180 | pendingDownloads.remove(oneFeedSpecifier) 181 | 182 | if let data = data, let response = response, response.statusIsOK, error == nil { 183 | if self.isFeed(data, oneFeedSpecifier.urlString) { 184 | self.addFeedSpecifier(oneFeedSpecifier) 185 | } 186 | } 187 | 188 | if pendingDownloads.isEmpty { 189 | self.stopFinding() 190 | } 191 | } 192 | } 193 | } 194 | 195 | func stopFinding() { 196 | 197 | notifyDelegateIfNeeded() 198 | } 199 | 200 | func notifyDelegateIfNeeded() { 201 | 202 | if !didNotifyDelegate { 203 | delegate?.feedFinder(self, didFindFeeds: Set(feedSpecifiers.values)) 204 | didNotifyDelegate = true 205 | } 206 | } 207 | 208 | func isFeed(_ data: Data, _ urlString: String) -> Bool { 209 | 210 | let parserData = ParserData(url: urlString, data: data) 211 | return FeedParser.canParse(parserData) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /FeedCurator/Sheets/AddFeed.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 56 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /FeedCurator/ViewController.swift: -------------------------------------------------------------------------------- 1 | //Copyright © 2019 Vincode, Inc. All rights reserved. 2 | 3 | import AppKit 4 | import RSCore 5 | import RSParser 6 | import RSWeb 7 | 8 | class ViewController: NSViewController, NSUserInterfaceValidations { 9 | 10 | @IBOutlet weak var outlineView: NSOutlineView! 11 | 12 | private var addFeed: AddFeed? 13 | private var feedFinder: FeedFinder? 14 | private var indeterminateProgress: IndeterminateProgress? 15 | private var numberOfFeedsBeingFound = 0 16 | 17 | private var windowController: WindowController? { 18 | return self.view.window?.windowController as? WindowController 19 | } 20 | 21 | var document: Document? { 22 | return windowController?.document as? Document 23 | } 24 | 25 | var currentlySelectedEntries: [OPMLEntry] { 26 | return outlineView.selectedRowIndexes.compactMap{ outlineView.item(atRow: $0) as? OPMLEntry } 27 | } 28 | 29 | var currentlySelectedParent: OPMLEntry? { 30 | guard currentlySelectedEntries.count == 1, let current = currentlySelectedEntries.first else { 31 | return nil 32 | } 33 | 34 | if current.isFolder { 35 | return current 36 | } else { 37 | return outlineView.parent(forItem: current) as? OPMLEntry 38 | } 39 | } 40 | 41 | typealias DragData = (parent: OPMLEntry?, index: Int) 42 | 43 | var currentDragData: DragData? 44 | 45 | override func viewDidLoad() { 46 | 47 | super.viewDidLoad() 48 | 49 | outlineView.delegate = self 50 | outlineView.dataSource = self 51 | outlineView.setDraggingSourceOperationMask(.copy, forLocal: false) 52 | outlineView.setDraggingSourceOperationMask(.move, forLocal: true) 53 | outlineView.registerForDraggedTypes([OPMLFeed.feedUTIType, OPMLEntry.folderUTIType, .URL, .string]) 54 | outlineView.allowsMultipleSelection = true 55 | 56 | NotificationCenter.default.addObserver(self, selector: #selector(opmlDocumentChildrenDidChange(_:)), name: .OPMLDocumentChildrenDidChange, object: nil) 57 | 58 | } 59 | 60 | override func viewDidAppear() { 61 | 62 | super.viewDidAppear() 63 | 64 | windowController!.titleButton.title = document?.opmlDocument.title ?? WindowController.clickHere 65 | outlineView.reloadData() 66 | 67 | } 68 | 69 | public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { 70 | if item.action == #selector(importOPML(_:)) { 71 | return true 72 | } 73 | 74 | if item.action == #selector(delete(_:)) { 75 | if currentlySelectedEntries.count > 0 { 76 | return true 77 | } 78 | } 79 | 80 | return false 81 | } 82 | 83 | // MARK: Actions 84 | 85 | @IBAction func delete(_ sender: AnyObject?) { 86 | for entry in currentlySelectedEntries { 87 | deleteEntry(entry) 88 | } 89 | } 90 | 91 | @IBAction func newFeed(_ sender: AnyObject?) { 92 | if let window = view.window { 93 | addFeed = AddFeed(delegate: self) 94 | addFeed!.runSheetOnWindow(window) 95 | } 96 | } 97 | 98 | @IBAction func newFolder(_ sender: AnyObject?) { 99 | let entry = OPMLEntry(title: NSLocalizedString("New Folder", comment: "New Folder")) 100 | insertEntry(entry) 101 | } 102 | 103 | @IBAction func renameEntry(_ sender: NSTextField) { 104 | guard let entry = currentlySelectedEntries.first else { 105 | return 106 | } 107 | document?.updateTitle(entry: entry, title: sender.stringValue) 108 | } 109 | 110 | @IBAction func importOPML(_ sender: AnyObject?) { 111 | let panel = NSOpenPanel() 112 | panel.canDownloadUbiquitousContents = true 113 | panel.canResolveUbiquitousConflicts = true 114 | panel.canChooseFiles = true 115 | panel.allowsMultipleSelection = true 116 | panel.canChooseDirectories = false 117 | panel.resolvesAliases = true 118 | panel.allowedFileTypes = ["opml", "xml"] 119 | panel.allowsOtherFileTypes = false 120 | 121 | panel.beginSheetModal(for: view.window!) { result in 122 | if result == NSApplication.ModalResponse.OK { 123 | for url in panel.urls { 124 | self.importOPML(url: url) 125 | } 126 | } 127 | } 128 | } 129 | 130 | func importOPML(url: URL) { 131 | guard let data = try? Data(contentsOf: url) else { 132 | return 133 | } 134 | 135 | let parserData = ParserData(url: "", data: data) 136 | guard let rsDoc = try? RSOPMLParser.parseOPML(with: parserData), 137 | let opmlDocument = rsDoc.translateToOPMLEntry(parent: nil) as? OPMLDocument else { 138 | return 139 | } 140 | 141 | let importFolder = OPMLEntry(title: opmlDocument.title ?? "") 142 | for child in opmlDocument.entries { 143 | importFolder.entries.append(child) 144 | } 145 | 146 | appendEntry(importFolder) 147 | } 148 | 149 | 150 | // MARK: Notifications 151 | @objc func opmlDocumentChildrenDidChange(_ note: Notification) { 152 | // Save the entry to restore the selection 153 | let selectedRowIndexes = outlineView.selectedRowIndexes 154 | outlineView.reloadData() 155 | outlineView.selectRowIndexes(selectedRowIndexes, byExtendingSelection: false) 156 | } 157 | 158 | } 159 | 160 | // MARK: AddFeedDelegate 161 | 162 | extension ViewController: AddFeedDelegate { 163 | 164 | func addFeedUserDidAdd(_ urlString: String) { 165 | findFeed(urlString) 166 | } 167 | 168 | func addFeedUserDidCancel() { 169 | } 170 | 171 | } 172 | 173 | // MARK: FeedFinderDelegate 174 | 175 | extension ViewController: FeedFinderDelegate { 176 | 177 | public func feedFinder(_ feedFinder: FeedFinder, didFindFeeds feedSpecifiers: Set) { 178 | numberOfFeedsBeingFound = numberOfFeedsBeingFound - 1 179 | view.window?.endSheet(indeterminateProgress!.window!) 180 | 181 | let dragData = currentDragData 182 | currentDragData = nil 183 | 184 | if let error = feedFinder.initialDownloadError { 185 | if feedFinder.initialDownloadStatusCode == 404 { 186 | showNoFeedsErrorMessage() 187 | } 188 | else { 189 | showInitialDownloadError(error) 190 | } 191 | return 192 | } 193 | 194 | guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers) else { 195 | showNoFeedsErrorMessage() 196 | return 197 | } 198 | 199 | if let url = URL(string: bestFeedSpecifier.urlString) { 200 | 201 | InitialFeedDownloader.download(url) { [weak self] (parsedFeed) in 202 | 203 | guard let parsedFeed = parsedFeed else { 204 | assertionFailure() 205 | return 206 | } 207 | 208 | let opmlFeed = OPMLFeed(title: parsedFeed.title, pageURL: parsedFeed.homePageURL, feedURL: bestFeedSpecifier.urlString) 209 | 210 | if dragData == nil { 211 | self?.insertEntry(opmlFeed) 212 | } else { 213 | self?.insertEntry(opmlFeed, parent: dragData!.parent, childIndex: dragData!.index) 214 | } 215 | 216 | } 217 | 218 | } else { 219 | showNoFeedsErrorMessage() 220 | } 221 | 222 | } 223 | 224 | private func showInitialDownloadError(_ error: Error) { 225 | 226 | let alert = NSAlert() 227 | alert.alertStyle = .informational 228 | alert.messageText = NSLocalizedString("Download Error", comment: "Feed finder") 229 | 230 | let formatString = NSLocalizedString("Can’t add this feed because of a download error: “%@”", comment: "Feed finder") 231 | let errorText = NSString.localizedStringWithFormat(formatString as NSString, error.localizedDescription) 232 | alert.informativeText = errorText as String 233 | 234 | if let window = view.window { 235 | alert.beginSheetModal(for: window) 236 | } 237 | 238 | } 239 | 240 | private func showNoFeedsErrorMessage() { 241 | 242 | let alert = NSAlert() 243 | alert.alertStyle = .informational 244 | alert.messageText = NSLocalizedString("Feed not found", comment: "Feed finder") 245 | alert.informativeText = NSLocalizedString("Can’t add a feed because no feed was found.", comment: "Feed finder") 246 | 247 | if let window = view.window { 248 | alert.beginSheetModal(for: window) 249 | } 250 | 251 | } 252 | 253 | } 254 | 255 | // MARK: API 256 | 257 | extension ViewController { 258 | 259 | func findFeed(_ urlString: String) { 260 | guard let url = URL(string: urlString.rs_normalizedURL()) else { 261 | return 262 | } 263 | 264 | if numberOfFeedsBeingFound == 0 { 265 | let msg = NSLocalizedString("Downloading feed data...", comment: "Downloading feed") 266 | indeterminateProgress = IndeterminateProgress(message: msg) 267 | view.window?.beginSheet(indeterminateProgress!.window!) 268 | } 269 | 270 | numberOfFeedsBeingFound = numberOfFeedsBeingFound + 1 271 | 272 | feedFinder = FeedFinder(url: url, delegate: self) 273 | } 274 | 275 | func deleteEntry(_ entry: OPMLEntry) { 276 | guard let document = document else { 277 | assertionFailure() 278 | return 279 | } 280 | 281 | let parent = outlineView.parent(forItem: entry) as? OPMLEntry 282 | let childIndex = outlineView.childIndex(forItem: entry) 283 | 284 | guard childIndex != -1 else { return } 285 | 286 | // Update the model 287 | let realParent = parent == nil ? document.opmlDocument : parent! 288 | document.removeEntry(parent: realParent, childIndex: childIndex) 289 | 290 | // Update the outline 291 | let indexSet = IndexSet(integer: childIndex) 292 | outlineView.removeItems(at: indexSet, inParent: parent, withAnimation: .slideUp) 293 | } 294 | 295 | func appendEntry(_ entry: OPMLEntry) { 296 | let childIndex = outlineView.numberOfChildren(ofItem: nil) 297 | insertEntry(entry, parent: nil, childIndex: childIndex) 298 | } 299 | 300 | func insertEntry(_ entry: OPMLEntry) { 301 | let parent = currentlySelectedParent 302 | let childIndex: Int = { 303 | if let current = currentlySelectedEntries.last { 304 | if current.isFolder { 305 | return outlineView.numberOfChildren(ofItem: parent) 306 | } else { 307 | return outlineView.childIndex(forItem: current) + 1 308 | } 309 | } else { 310 | return outlineView.numberOfChildren(ofItem: parent) 311 | } 312 | }() 313 | 314 | insertEntry(entry, parent: parent, childIndex: childIndex) 315 | 316 | } 317 | 318 | func insertEntry(_ entry: OPMLEntry, parent: OPMLEntry?, childIndex: Int) { 319 | 320 | guard let document = document else { 321 | assertionFailure() 322 | return 323 | } 324 | 325 | let correctedChildIndex: Int = { 326 | if childIndex == NSOutlineViewDropOnItemIndex { 327 | return parent?.entries.count ?? document.opmlDocument.entries.count 328 | } else { 329 | return childIndex 330 | } 331 | }() 332 | 333 | // Update the model 334 | let realParent = parent == nil ? document.opmlDocument : parent! 335 | document.insertEntry(parent: realParent, childIndex: correctedChildIndex, entry: entry) 336 | 337 | // Update the outline 338 | let indexSet = IndexSet(integer: correctedChildIndex) 339 | outlineView.insertItems(at: indexSet, inParent: parent, withAnimation: .slideDown) 340 | 341 | outlineView.expandItem(parent, expandChildren: false) 342 | let rowIndex = outlineView.row(forItem: entry) 343 | outlineView.rs_selectRowAndScrollToVisible(rowIndex) 344 | 345 | } 346 | 347 | func moveEntry(_ entry: OPMLEntry, toParent: OPMLEntry?, toChildIndex: Int) { 348 | 349 | guard let document = document else { 350 | assertionFailure() 351 | return 352 | } 353 | 354 | let fromParent = outlineView.parent(forItem: entry) as? OPMLEntry 355 | let fromChildIndex = outlineView.childIndex(forItem: entry) 356 | 357 | let correctedToChildIndex: Int = { 358 | var corrected = 0 359 | if toChildIndex == NSOutlineViewDropOnItemIndex { 360 | corrected = toParent?.entries.count ?? document.opmlDocument.entries.count 361 | } else { 362 | corrected = toChildIndex 363 | } 364 | if fromParent ?? document.opmlDocument == toParent ?? document.opmlDocument && fromChildIndex < corrected { 365 | corrected = corrected - 1 366 | } 367 | return corrected 368 | }() 369 | 370 | document.moveEntry(fromParent: fromParent ?? document.opmlDocument, 371 | fromChildIndex: fromChildIndex, 372 | toParent: toParent ?? document.opmlDocument, 373 | toChildIndex: correctedToChildIndex, 374 | entry: entry) 375 | 376 | // Update the outline 377 | outlineView.moveItem(at: fromChildIndex, inParent: fromParent, to: correctedToChildIndex, inParent: toParent) 378 | } 379 | 380 | } 381 | -------------------------------------------------------------------------------- /Feed Curator.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 512BDE25224687CF00CB4159 /* UpdateTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512BDE23224687CF00CB4159 /* UpdateTitle.swift */; }; 11 | 512BDE26224687CF00CB4159 /* UpdateTitle.xib in Resources */ = {isa = PBXBuildFile; fileRef = 512BDE24224687CF00CB4159 /* UpdateTitle.xib */; }; 12 | 512BDE29224689DE00CB4159 /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512BDE28224689DE00CB4159 /* String+.swift */; }; 13 | 51533745224E9BE10024544D /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51533744224E9BE10024544D /* Credits.rtf */; }; 14 | 5153381B2252BB800024544D /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5153381A2252BB800024544D /* AppDefaults.swift */; }; 15 | 518C65A722443C7B00B18604 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C65A622443C7B00B18604 /* AppDelegate.swift */; }; 16 | 518C65A922443C7B00B18604 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C65A822443C7B00B18604 /* ViewController.swift */; }; 17 | 518C65AB22443C7B00B18604 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C65AA22443C7B00B18604 /* Document.swift */; }; 18 | 518C65AD22443C7C00B18604 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 518C65AC22443C7C00B18604 /* Assets.xcassets */; }; 19 | 518C65B022443C7C00B18604 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 518C65AE22443C7C00B18604 /* Main.storyboard */; }; 20 | 518C65D522444FDA00B18604 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 518C65C222443FF500B18604 /* RSParser.framework */; }; 21 | 518C65D622444FDA00B18604 /* RSParser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 518C65C222443FF500B18604 /* RSParser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 22 | 518C65F9224577AF00B18604 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 518C65F4224577A100B18604 /* RSCore.framework */; }; 23 | 518C65FA224577AF00B18604 /* RSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 518C65F4224577A100B18604 /* RSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 24 | 518C65FD224577B600B18604 /* RSWeb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 518C65E82245779900B18604 /* RSWeb.framework */; }; 25 | 518C65FE224577B600B18604 /* RSWeb.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 518C65E82245779900B18604 /* RSWeb.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 26 | 518C66022245791600B18604 /* OutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C66012245791600B18604 /* OutlineView.swift */; }; 27 | 518C66042245834E00B18604 /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C66032245834E00B18604 /* WindowController.swift */; }; 28 | 518C66222245A5A400B18604 /* OPMLFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C661E2245A5A400B18604 /* OPMLFeed.swift */; }; 29 | 518C66232245A5A400B18604 /* RSOPMLItem+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C661F2245A5A400B18604 /* RSOPMLItem+.swift */; }; 30 | 518C66242245A5A400B18604 /* OPMLDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C66202245A5A400B18604 /* OPMLDocument.swift */; }; 31 | 518C66252245A5A400B18604 /* OPMLEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C66212245A5A400B18604 /* OPMLEntry.swift */; }; 32 | 51A35B5E224A50470098FA13 /* IndeterminateProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A35B5C224A50470098FA13 /* IndeterminateProgress.swift */; }; 33 | 51A35B5F224A50470098FA13 /* IndeterminateProgress.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A35B5D224A50470098FA13 /* IndeterminateProgress.xib */; }; 34 | 51E41A172247CCB30014DDCC /* AddFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E41A152247CCB30014DDCC /* AddFeed.swift */; }; 35 | 51E41A182247CCB30014DDCC /* AddFeed.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E41A162247CCB30014DDCC /* AddFeed.xib */; }; 36 | 51E41A1D224932B00014DDCC /* FeedFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E41A1A224932B00014DDCC /* FeedFinder.swift */; }; 37 | 51E41A1E224932B00014DDCC /* HTMLFeedFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E41A1B224932B00014DDCC /* HTMLFeedFinder.swift */; }; 38 | 51E41A1F224932B00014DDCC /* FeedSpecifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E41A1C224932B00014DDCC /* FeedSpecifier.swift */; }; 39 | 51E41A2122497DC80014DDCC /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E41A2022497DC80014DDCC /* InitialFeedDownloader.swift */; }; 40 | 51E41A2322497E2E0014DDCC /* ViewController+OutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E41A2222497E2E0014DDCC /* ViewController+OutlineView.swift */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXContainerItemProxy section */ 44 | 518C65C122443FF500B18604 /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = 518C65BC22443FF500B18604 /* RSParser.xcodeproj */; 47 | proxyType = 2; 48 | remoteGlobalIDString = 84FF5F841EFA285800C15A01; 49 | remoteInfo = RSParser; 50 | }; 51 | 518C65C322443FF500B18604 /* PBXContainerItemProxy */ = { 52 | isa = PBXContainerItemProxy; 53 | containerPortal = 518C65BC22443FF500B18604 /* RSParser.xcodeproj */; 54 | proxyType = 2; 55 | remoteGlobalIDString = 84FF5F8D1EFA285800C15A01; 56 | remoteInfo = RSParserTests; 57 | }; 58 | 518C65D722444FDA00B18604 /* PBXContainerItemProxy */ = { 59 | isa = PBXContainerItemProxy; 60 | containerPortal = 518C65BC22443FF500B18604 /* RSParser.xcodeproj */; 61 | proxyType = 1; 62 | remoteGlobalIDString = 84FF5F831EFA285800C15A01; 63 | remoteInfo = RSParser; 64 | }; 65 | 518C65E72245779900B18604 /* PBXContainerItemProxy */ = { 66 | isa = PBXContainerItemProxy; 67 | containerPortal = 518C65E02245779900B18604 /* RSWeb.xcodeproj */; 68 | proxyType = 2; 69 | remoteGlobalIDString = 849C08B61E0CAC85006B03FA; 70 | remoteInfo = RSWeb; 71 | }; 72 | 518C65E92245779900B18604 /* PBXContainerItemProxy */ = { 73 | isa = PBXContainerItemProxy; 74 | containerPortal = 518C65E02245779900B18604 /* RSWeb.xcodeproj */; 75 | proxyType = 2; 76 | remoteGlobalIDString = 849C08BF1E0CAC86006B03FA; 77 | remoteInfo = RSWebTests; 78 | }; 79 | 518C65EB2245779900B18604 /* PBXContainerItemProxy */ = { 80 | isa = PBXContainerItemProxy; 81 | containerPortal = 518C65E02245779900B18604 /* RSWeb.xcodeproj */; 82 | proxyType = 2; 83 | remoteGlobalIDString = 849C08D51E0CACA3006B03FA; 84 | remoteInfo = RSWebiOS; 85 | }; 86 | 518C65F3224577A100B18604 /* PBXContainerItemProxy */ = { 87 | isa = PBXContainerItemProxy; 88 | containerPortal = 518C65ED224577A100B18604 /* RSCore.xcodeproj */; 89 | proxyType = 2; 90 | remoteGlobalIDString = 84CFF4F41AC3C69700CEA6C8; 91 | remoteInfo = RSCore; 92 | }; 93 | 518C65F5224577A100B18604 /* PBXContainerItemProxy */ = { 94 | isa = PBXContainerItemProxy; 95 | containerPortal = 518C65ED224577A100B18604 /* RSCore.xcodeproj */; 96 | proxyType = 2; 97 | remoteGlobalIDString = 84CFF4FF1AC3C69700CEA6C8; 98 | remoteInfo = RSCoreTests; 99 | }; 100 | 518C65F7224577A100B18604 /* PBXContainerItemProxy */ = { 101 | isa = PBXContainerItemProxy; 102 | containerPortal = 518C65ED224577A100B18604 /* RSCore.xcodeproj */; 103 | proxyType = 2; 104 | remoteGlobalIDString = 842DD7BC1E14993900E061EB; 105 | remoteInfo = RSCoreiOS; 106 | }; 107 | 518C65FB224577AF00B18604 /* PBXContainerItemProxy */ = { 108 | isa = PBXContainerItemProxy; 109 | containerPortal = 518C65ED224577A100B18604 /* RSCore.xcodeproj */; 110 | proxyType = 1; 111 | remoteGlobalIDString = 84CFF4F31AC3C69700CEA6C8; 112 | remoteInfo = RSCore; 113 | }; 114 | 518C65FF224577B700B18604 /* PBXContainerItemProxy */ = { 115 | isa = PBXContainerItemProxy; 116 | containerPortal = 518C65E02245779900B18604 /* RSWeb.xcodeproj */; 117 | proxyType = 1; 118 | remoteGlobalIDString = 849C08B51E0CAC85006B03FA; 119 | remoteInfo = RSWeb; 120 | }; 121 | /* End PBXContainerItemProxy section */ 122 | 123 | /* Begin PBXCopyFilesBuildPhase section */ 124 | 518C65D922444FDA00B18604 /* Embed Frameworks */ = { 125 | isa = PBXCopyFilesBuildPhase; 126 | buildActionMask = 2147483647; 127 | dstPath = ""; 128 | dstSubfolderSpec = 10; 129 | files = ( 130 | 518C65FE224577B600B18604 /* RSWeb.framework in Embed Frameworks */, 131 | 518C65FA224577AF00B18604 /* RSCore.framework in Embed Frameworks */, 132 | 518C65D622444FDA00B18604 /* RSParser.framework in Embed Frameworks */, 133 | ); 134 | name = "Embed Frameworks"; 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXCopyFilesBuildPhase section */ 138 | 139 | /* Begin PBXFileReference section */ 140 | 512BDE23224687CF00CB4159 /* UpdateTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTitle.swift; sourceTree = ""; }; 141 | 512BDE24224687CF00CB4159 /* UpdateTitle.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UpdateTitle.xib; sourceTree = ""; }; 142 | 512BDE28224689DE00CB4159 /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; 143 | 512BDE2A224694E800CB4159 /* Feed Curator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Feed Curator.entitlements"; sourceTree = ""; }; 144 | 51533744224E9BE10024544D /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 145 | 5153381A2252BB800024544D /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = ""; }; 146 | 518C65A322443C7B00B18604 /* Feed Curator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Feed Curator.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 147 | 518C65A622443C7B00B18604 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 148 | 518C65A822443C7B00B18604 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 149 | 518C65AA22443C7B00B18604 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; 150 | 518C65AC22443C7C00B18604 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 151 | 518C65AF22443C7C00B18604 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 152 | 518C65B122443C7C00B18604 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 153 | 518C65B222443C7C00B18604 /* FeedCurator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FeedCurator.entitlements; sourceTree = ""; }; 154 | 518C65BC22443FF500B18604 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = submodules/RSParser/RSParser.xcodeproj; sourceTree = ""; }; 155 | 518C65E02245779900B18604 /* RSWeb.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSWeb.xcodeproj; path = submodules/RSWeb/RSWeb.xcodeproj; sourceTree = ""; }; 156 | 518C65ED224577A100B18604 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = submodules/RSCore/RSCore.xcodeproj; sourceTree = ""; }; 157 | 518C66012245791600B18604 /* OutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineView.swift; sourceTree = ""; }; 158 | 518C66032245834E00B18604 /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; 159 | 518C661E2245A5A400B18604 /* OPMLFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPMLFeed.swift; sourceTree = ""; }; 160 | 518C661F2245A5A400B18604 /* RSOPMLItem+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RSOPMLItem+.swift"; sourceTree = ""; }; 161 | 518C66202245A5A400B18604 /* OPMLDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPMLDocument.swift; sourceTree = ""; }; 162 | 518C66212245A5A400B18604 /* OPMLEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPMLEntry.swift; sourceTree = ""; }; 163 | 51A35B5C224A50470098FA13 /* IndeterminateProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgress.swift; sourceTree = ""; }; 164 | 51A35B5D224A50470098FA13 /* IndeterminateProgress.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IndeterminateProgress.xib; sourceTree = ""; }; 165 | 51E41A152247CCB30014DDCC /* AddFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeed.swift; sourceTree = ""; }; 166 | 51E41A162247CCB30014DDCC /* AddFeed.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddFeed.xib; sourceTree = ""; }; 167 | 51E41A1A224932B00014DDCC /* FeedFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedFinder.swift; sourceTree = ""; }; 168 | 51E41A1B224932B00014DDCC /* HTMLFeedFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLFeedFinder.swift; sourceTree = ""; }; 169 | 51E41A1C224932B00014DDCC /* FeedSpecifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedSpecifier.swift; sourceTree = ""; }; 170 | 51E41A2022497DC80014DDCC /* InitialFeedDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialFeedDownloader.swift; sourceTree = ""; }; 171 | 51E41A2222497E2E0014DDCC /* ViewController+OutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+OutlineView.swift"; sourceTree = ""; }; 172 | /* End PBXFileReference section */ 173 | 174 | /* Begin PBXFrameworksBuildPhase section */ 175 | 518C65A022443C7B00B18604 /* Frameworks */ = { 176 | isa = PBXFrameworksBuildPhase; 177 | buildActionMask = 2147483647; 178 | files = ( 179 | 518C65FD224577B600B18604 /* RSWeb.framework in Frameworks */, 180 | 518C65F9224577AF00B18604 /* RSCore.framework in Frameworks */, 181 | 518C65D522444FDA00B18604 /* RSParser.framework in Frameworks */, 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | }; 185 | /* End PBXFrameworksBuildPhase section */ 186 | 187 | /* Begin PBXGroup section */ 188 | 512BDE1D2246879B00CB4159 /* Sheets */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | 51E41A152247CCB30014DDCC /* AddFeed.swift */, 192 | 51E41A162247CCB30014DDCC /* AddFeed.xib */, 193 | 51A35B5C224A50470098FA13 /* IndeterminateProgress.swift */, 194 | 51A35B5D224A50470098FA13 /* IndeterminateProgress.xib */, 195 | 512BDE23224687CF00CB4159 /* UpdateTitle.swift */, 196 | 512BDE24224687CF00CB4159 /* UpdateTitle.xib */, 197 | ); 198 | path = Sheets; 199 | sourceTree = ""; 200 | }; 201 | 512BDE27224689C400CB4159 /* Extensions */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 512BDE28224689DE00CB4159 /* String+.swift */, 205 | ); 206 | path = Extensions; 207 | sourceTree = ""; 208 | }; 209 | 518C659A22443C7B00B18604 = { 210 | isa = PBXGroup; 211 | children = ( 212 | 512BDE2A224694E800CB4159 /* Feed Curator.entitlements */, 213 | 518C65A522443C7B00B18604 /* FeedCurator */, 214 | 518C65A422443C7B00B18604 /* Products */, 215 | 518C65ED224577A100B18604 /* RSCore.xcodeproj */, 216 | 518C65BC22443FF500B18604 /* RSParser.xcodeproj */, 217 | 518C65E02245779900B18604 /* RSWeb.xcodeproj */, 218 | ); 219 | sourceTree = ""; 220 | }; 221 | 518C65A422443C7B00B18604 /* Products */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | 518C65A322443C7B00B18604 /* Feed Curator.app */, 225 | ); 226 | name = Products; 227 | sourceTree = ""; 228 | }; 229 | 518C65A522443C7B00B18604 /* FeedCurator */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | 518C65AE22443C7C00B18604 /* Main.storyboard */, 233 | 518C65A622443C7B00B18604 /* AppDelegate.swift */, 234 | 5153381A2252BB800024544D /* AppDefaults.swift */, 235 | 518C65AA22443C7B00B18604 /* Document.swift */, 236 | 518C66032245834E00B18604 /* WindowController.swift */, 237 | 518C65A822443C7B00B18604 /* ViewController.swift */, 238 | 51E41A2222497E2E0014DDCC /* ViewController+OutlineView.swift */, 239 | 518C66012245791600B18604 /* OutlineView.swift */, 240 | 512BDE1D2246879B00CB4159 /* Sheets */, 241 | 518C661D2245A51E00B18604 /* Model */, 242 | 51E41A19224932B00014DDCC /* FeedFinder */, 243 | 512BDE27224689C400CB4159 /* Extensions */, 244 | 518C65AC22443C7C00B18604 /* Assets.xcassets */, 245 | 518C65B122443C7C00B18604 /* Info.plist */, 246 | 518C65B222443C7C00B18604 /* FeedCurator.entitlements */, 247 | 51533744224E9BE10024544D /* Credits.rtf */, 248 | ); 249 | path = FeedCurator; 250 | sourceTree = ""; 251 | }; 252 | 518C65BD22443FF500B18604 /* Products */ = { 253 | isa = PBXGroup; 254 | children = ( 255 | 518C65C222443FF500B18604 /* RSParser.framework */, 256 | 518C65C422443FF500B18604 /* RSParserTests.xctest */, 257 | ); 258 | name = Products; 259 | sourceTree = ""; 260 | }; 261 | 518C65E12245779900B18604 /* Products */ = { 262 | isa = PBXGroup; 263 | children = ( 264 | 518C65E82245779900B18604 /* RSWeb.framework */, 265 | 518C65EA2245779900B18604 /* RSWebTests.xctest */, 266 | 518C65EC2245779900B18604 /* RSWeb.framework */, 267 | ); 268 | name = Products; 269 | sourceTree = ""; 270 | }; 271 | 518C65EE224577A100B18604 /* Products */ = { 272 | isa = PBXGroup; 273 | children = ( 274 | 518C65F4224577A100B18604 /* RSCore.framework */, 275 | 518C65F6224577A100B18604 /* RSCoreTests.xctest */, 276 | 518C65F8224577A100B18604 /* RSCore.framework */, 277 | ); 278 | name = Products; 279 | sourceTree = ""; 280 | }; 281 | 518C661D2245A51E00B18604 /* Model */ = { 282 | isa = PBXGroup; 283 | children = ( 284 | 518C66202245A5A400B18604 /* OPMLDocument.swift */, 285 | 518C66212245A5A400B18604 /* OPMLEntry.swift */, 286 | 518C661E2245A5A400B18604 /* OPMLFeed.swift */, 287 | 518C661F2245A5A400B18604 /* RSOPMLItem+.swift */, 288 | ); 289 | path = Model; 290 | sourceTree = ""; 291 | }; 292 | 51E41A19224932B00014DDCC /* FeedFinder */ = { 293 | isa = PBXGroup; 294 | children = ( 295 | 51E41A1A224932B00014DDCC /* FeedFinder.swift */, 296 | 51E41A1B224932B00014DDCC /* HTMLFeedFinder.swift */, 297 | 51E41A1C224932B00014DDCC /* FeedSpecifier.swift */, 298 | 51E41A2022497DC80014DDCC /* InitialFeedDownloader.swift */, 299 | ); 300 | path = FeedFinder; 301 | sourceTree = ""; 302 | }; 303 | /* End PBXGroup section */ 304 | 305 | /* Begin PBXNativeTarget section */ 306 | 518C65A222443C7B00B18604 /* Feed Curator */ = { 307 | isa = PBXNativeTarget; 308 | buildConfigurationList = 518C65B522443C7C00B18604 /* Build configuration list for PBXNativeTarget "Feed Curator" */; 309 | buildPhases = ( 310 | 518C65BB22443D1400B18604 /* Increment Build Number */, 311 | 518C659F22443C7B00B18604 /* Sources */, 312 | 518C65A022443C7B00B18604 /* Frameworks */, 313 | 518C65A122443C7B00B18604 /* Resources */, 314 | 518C65D922444FDA00B18604 /* Embed Frameworks */, 315 | ); 316 | buildRules = ( 317 | ); 318 | dependencies = ( 319 | 518C65D822444FDA00B18604 /* PBXTargetDependency */, 320 | 518C65FC224577AF00B18604 /* PBXTargetDependency */, 321 | 518C6600224577B700B18604 /* PBXTargetDependency */, 322 | ); 323 | name = "Feed Curator"; 324 | productName = FeedCurator; 325 | productReference = 518C65A322443C7B00B18604 /* Feed Curator.app */; 326 | productType = "com.apple.product-type.application"; 327 | }; 328 | /* End PBXNativeTarget section */ 329 | 330 | /* Begin PBXProject section */ 331 | 518C659B22443C7B00B18604 /* Project object */ = { 332 | isa = PBXProject; 333 | attributes = { 334 | LastSwiftUpdateCheck = 1010; 335 | LastUpgradeCheck = 1010; 336 | ORGANIZATIONNAME = "Vincode, Inc."; 337 | TargetAttributes = { 338 | 518C65A222443C7B00B18604 = { 339 | CreatedOnToolsVersion = 10.1; 340 | SystemCapabilities = { 341 | com.apple.HardenedRuntime = { 342 | enabled = 1; 343 | }; 344 | com.apple.Sandbox = { 345 | enabled = 1; 346 | }; 347 | }; 348 | }; 349 | }; 350 | }; 351 | buildConfigurationList = 518C659E22443C7B00B18604 /* Build configuration list for PBXProject "Feed Curator" */; 352 | compatibilityVersion = "Xcode 9.3"; 353 | developmentRegion = en; 354 | hasScannedForEncodings = 0; 355 | knownRegions = ( 356 | en, 357 | Base, 358 | ); 359 | mainGroup = 518C659A22443C7B00B18604; 360 | productRefGroup = 518C65A422443C7B00B18604 /* Products */; 361 | projectDirPath = ""; 362 | projectReferences = ( 363 | { 364 | ProductGroup = 518C65EE224577A100B18604 /* Products */; 365 | ProjectRef = 518C65ED224577A100B18604 /* RSCore.xcodeproj */; 366 | }, 367 | { 368 | ProductGroup = 518C65BD22443FF500B18604 /* Products */; 369 | ProjectRef = 518C65BC22443FF500B18604 /* RSParser.xcodeproj */; 370 | }, 371 | { 372 | ProductGroup = 518C65E12245779900B18604 /* Products */; 373 | ProjectRef = 518C65E02245779900B18604 /* RSWeb.xcodeproj */; 374 | }, 375 | ); 376 | projectRoot = ""; 377 | targets = ( 378 | 518C65A222443C7B00B18604 /* Feed Curator */, 379 | ); 380 | }; 381 | /* End PBXProject section */ 382 | 383 | /* Begin PBXReferenceProxy section */ 384 | 518C65C222443FF500B18604 /* RSParser.framework */ = { 385 | isa = PBXReferenceProxy; 386 | fileType = wrapper.framework; 387 | path = RSParser.framework; 388 | remoteRef = 518C65C122443FF500B18604 /* PBXContainerItemProxy */; 389 | sourceTree = BUILT_PRODUCTS_DIR; 390 | }; 391 | 518C65C422443FF500B18604 /* RSParserTests.xctest */ = { 392 | isa = PBXReferenceProxy; 393 | fileType = wrapper.cfbundle; 394 | path = RSParserTests.xctest; 395 | remoteRef = 518C65C322443FF500B18604 /* PBXContainerItemProxy */; 396 | sourceTree = BUILT_PRODUCTS_DIR; 397 | }; 398 | 518C65E82245779900B18604 /* RSWeb.framework */ = { 399 | isa = PBXReferenceProxy; 400 | fileType = wrapper.framework; 401 | path = RSWeb.framework; 402 | remoteRef = 518C65E72245779900B18604 /* PBXContainerItemProxy */; 403 | sourceTree = BUILT_PRODUCTS_DIR; 404 | }; 405 | 518C65EA2245779900B18604 /* RSWebTests.xctest */ = { 406 | isa = PBXReferenceProxy; 407 | fileType = wrapper.cfbundle; 408 | path = RSWebTests.xctest; 409 | remoteRef = 518C65E92245779900B18604 /* PBXContainerItemProxy */; 410 | sourceTree = BUILT_PRODUCTS_DIR; 411 | }; 412 | 518C65EC2245779900B18604 /* RSWeb.framework */ = { 413 | isa = PBXReferenceProxy; 414 | fileType = wrapper.framework; 415 | path = RSWeb.framework; 416 | remoteRef = 518C65EB2245779900B18604 /* PBXContainerItemProxy */; 417 | sourceTree = BUILT_PRODUCTS_DIR; 418 | }; 419 | 518C65F4224577A100B18604 /* RSCore.framework */ = { 420 | isa = PBXReferenceProxy; 421 | fileType = wrapper.framework; 422 | path = RSCore.framework; 423 | remoteRef = 518C65F3224577A100B18604 /* PBXContainerItemProxy */; 424 | sourceTree = BUILT_PRODUCTS_DIR; 425 | }; 426 | 518C65F6224577A100B18604 /* RSCoreTests.xctest */ = { 427 | isa = PBXReferenceProxy; 428 | fileType = wrapper.cfbundle; 429 | path = RSCoreTests.xctest; 430 | remoteRef = 518C65F5224577A100B18604 /* PBXContainerItemProxy */; 431 | sourceTree = BUILT_PRODUCTS_DIR; 432 | }; 433 | 518C65F8224577A100B18604 /* RSCore.framework */ = { 434 | isa = PBXReferenceProxy; 435 | fileType = wrapper.framework; 436 | path = RSCore.framework; 437 | remoteRef = 518C65F7224577A100B18604 /* PBXContainerItemProxy */; 438 | sourceTree = BUILT_PRODUCTS_DIR; 439 | }; 440 | /* End PBXReferenceProxy section */ 441 | 442 | /* Begin PBXResourcesBuildPhase section */ 443 | 518C65A122443C7B00B18604 /* Resources */ = { 444 | isa = PBXResourcesBuildPhase; 445 | buildActionMask = 2147483647; 446 | files = ( 447 | 51E41A182247CCB30014DDCC /* AddFeed.xib in Resources */, 448 | 51533745224E9BE10024544D /* Credits.rtf in Resources */, 449 | 518C65AD22443C7C00B18604 /* Assets.xcassets in Resources */, 450 | 518C65B022443C7C00B18604 /* Main.storyboard in Resources */, 451 | 51A35B5F224A50470098FA13 /* IndeterminateProgress.xib in Resources */, 452 | 512BDE26224687CF00CB4159 /* UpdateTitle.xib in Resources */, 453 | ); 454 | runOnlyForDeploymentPostprocessing = 0; 455 | }; 456 | /* End PBXResourcesBuildPhase section */ 457 | 458 | /* Begin PBXShellScriptBuildPhase section */ 459 | 518C65BB22443D1400B18604 /* Increment Build Number */ = { 460 | isa = PBXShellScriptBuildPhase; 461 | buildActionMask = 2147483647; 462 | files = ( 463 | ); 464 | inputFileListPaths = ( 465 | ); 466 | inputPaths = ( 467 | ); 468 | name = "Increment Build Number"; 469 | outputFileListPaths = ( 470 | ); 471 | outputPaths = ( 472 | ); 473 | runOnlyForDeploymentPostprocessing = 0; 474 | shellPath = /bin/sh; 475 | shellScript = "buildNumber=$(/usr/libexec/PlistBuddy -c \"Print CFBundleVersion\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\")\nbuildNumber=$(($buildNumber + 1))\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\"\n"; 476 | }; 477 | /* End PBXShellScriptBuildPhase section */ 478 | 479 | /* Begin PBXSourcesBuildPhase section */ 480 | 518C659F22443C7B00B18604 /* Sources */ = { 481 | isa = PBXSourcesBuildPhase; 482 | buildActionMask = 2147483647; 483 | files = ( 484 | 518C65A922443C7B00B18604 /* ViewController.swift in Sources */, 485 | 518C66222245A5A400B18604 /* OPMLFeed.swift in Sources */, 486 | 518C66252245A5A400B18604 /* OPMLEntry.swift in Sources */, 487 | 51E41A2122497DC80014DDCC /* InitialFeedDownloader.swift in Sources */, 488 | 51E41A1D224932B00014DDCC /* FeedFinder.swift in Sources */, 489 | 51E41A172247CCB30014DDCC /* AddFeed.swift in Sources */, 490 | 512BDE25224687CF00CB4159 /* UpdateTitle.swift in Sources */, 491 | 518C66042245834E00B18604 /* WindowController.swift in Sources */, 492 | 518C65A722443C7B00B18604 /* AppDelegate.swift in Sources */, 493 | 51E41A1E224932B00014DDCC /* HTMLFeedFinder.swift in Sources */, 494 | 51A35B5E224A50470098FA13 /* IndeterminateProgress.swift in Sources */, 495 | 51E41A1F224932B00014DDCC /* FeedSpecifier.swift in Sources */, 496 | 518C66242245A5A400B18604 /* OPMLDocument.swift in Sources */, 497 | 512BDE29224689DE00CB4159 /* String+.swift in Sources */, 498 | 518C66232245A5A400B18604 /* RSOPMLItem+.swift in Sources */, 499 | 5153381B2252BB800024544D /* AppDefaults.swift in Sources */, 500 | 518C65AB22443C7B00B18604 /* Document.swift in Sources */, 501 | 518C66022245791600B18604 /* OutlineView.swift in Sources */, 502 | 51E41A2322497E2E0014DDCC /* ViewController+OutlineView.swift in Sources */, 503 | ); 504 | runOnlyForDeploymentPostprocessing = 0; 505 | }; 506 | /* End PBXSourcesBuildPhase section */ 507 | 508 | /* Begin PBXTargetDependency section */ 509 | 518C65D822444FDA00B18604 /* PBXTargetDependency */ = { 510 | isa = PBXTargetDependency; 511 | name = RSParser; 512 | targetProxy = 518C65D722444FDA00B18604 /* PBXContainerItemProxy */; 513 | }; 514 | 518C65FC224577AF00B18604 /* PBXTargetDependency */ = { 515 | isa = PBXTargetDependency; 516 | name = RSCore; 517 | targetProxy = 518C65FB224577AF00B18604 /* PBXContainerItemProxy */; 518 | }; 519 | 518C6600224577B700B18604 /* PBXTargetDependency */ = { 520 | isa = PBXTargetDependency; 521 | name = RSWeb; 522 | targetProxy = 518C65FF224577B700B18604 /* PBXContainerItemProxy */; 523 | }; 524 | /* End PBXTargetDependency section */ 525 | 526 | /* Begin PBXVariantGroup section */ 527 | 518C65AE22443C7C00B18604 /* Main.storyboard */ = { 528 | isa = PBXVariantGroup; 529 | children = ( 530 | 518C65AF22443C7C00B18604 /* Base */, 531 | ); 532 | name = Main.storyboard; 533 | sourceTree = ""; 534 | }; 535 | /* End PBXVariantGroup section */ 536 | 537 | /* Begin XCBuildConfiguration section */ 538 | 518C65B322443C7C00B18604 /* Debug */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | ALWAYS_SEARCH_USER_PATHS = NO; 542 | CLANG_ANALYZER_NONNULL = YES; 543 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 544 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 545 | CLANG_CXX_LIBRARY = "libc++"; 546 | CLANG_ENABLE_MODULES = YES; 547 | CLANG_ENABLE_OBJC_ARC = YES; 548 | CLANG_ENABLE_OBJC_WEAK = YES; 549 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 550 | CLANG_WARN_BOOL_CONVERSION = YES; 551 | CLANG_WARN_COMMA = YES; 552 | CLANG_WARN_CONSTANT_CONVERSION = YES; 553 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 554 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 555 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 556 | CLANG_WARN_EMPTY_BODY = YES; 557 | CLANG_WARN_ENUM_CONVERSION = YES; 558 | CLANG_WARN_INFINITE_RECURSION = YES; 559 | CLANG_WARN_INT_CONVERSION = YES; 560 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 561 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 562 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 563 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 564 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 565 | CLANG_WARN_STRICT_PROTOTYPES = YES; 566 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 567 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 568 | CLANG_WARN_UNREACHABLE_CODE = YES; 569 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 570 | CODE_SIGN_IDENTITY = "Mac Developer"; 571 | COPY_PHASE_STRIP = NO; 572 | DEBUG_INFORMATION_FORMAT = dwarf; 573 | ENABLE_STRICT_OBJC_MSGSEND = YES; 574 | ENABLE_TESTABILITY = YES; 575 | GCC_C_LANGUAGE_STANDARD = gnu11; 576 | GCC_DYNAMIC_NO_PIC = NO; 577 | GCC_NO_COMMON_BLOCKS = YES; 578 | GCC_OPTIMIZATION_LEVEL = 0; 579 | GCC_PREPROCESSOR_DEFINITIONS = ( 580 | "DEBUG=1", 581 | "$(inherited)", 582 | ); 583 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 584 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 585 | GCC_WARN_UNDECLARED_SELECTOR = YES; 586 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 587 | GCC_WARN_UNUSED_FUNCTION = YES; 588 | GCC_WARN_UNUSED_VARIABLE = YES; 589 | MACOSX_DEPLOYMENT_TARGET = 10.14; 590 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 591 | MTL_FAST_MATH = YES; 592 | ONLY_ACTIVE_ARCH = YES; 593 | SDKROOT = macosx; 594 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 595 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 596 | }; 597 | name = Debug; 598 | }; 599 | 518C65B422443C7C00B18604 /* Release */ = { 600 | isa = XCBuildConfiguration; 601 | buildSettings = { 602 | ALWAYS_SEARCH_USER_PATHS = NO; 603 | CLANG_ANALYZER_NONNULL = YES; 604 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 605 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 606 | CLANG_CXX_LIBRARY = "libc++"; 607 | CLANG_ENABLE_MODULES = YES; 608 | CLANG_ENABLE_OBJC_ARC = YES; 609 | CLANG_ENABLE_OBJC_WEAK = YES; 610 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 611 | CLANG_WARN_BOOL_CONVERSION = YES; 612 | CLANG_WARN_COMMA = YES; 613 | CLANG_WARN_CONSTANT_CONVERSION = YES; 614 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 615 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 616 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 617 | CLANG_WARN_EMPTY_BODY = YES; 618 | CLANG_WARN_ENUM_CONVERSION = YES; 619 | CLANG_WARN_INFINITE_RECURSION = YES; 620 | CLANG_WARN_INT_CONVERSION = YES; 621 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 622 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 623 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 624 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 625 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 626 | CLANG_WARN_STRICT_PROTOTYPES = YES; 627 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 628 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 629 | CLANG_WARN_UNREACHABLE_CODE = YES; 630 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 631 | CODE_SIGN_IDENTITY = "Mac Developer"; 632 | COPY_PHASE_STRIP = NO; 633 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 634 | ENABLE_NS_ASSERTIONS = NO; 635 | ENABLE_STRICT_OBJC_MSGSEND = YES; 636 | GCC_C_LANGUAGE_STANDARD = gnu11; 637 | GCC_NO_COMMON_BLOCKS = YES; 638 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 639 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 640 | GCC_WARN_UNDECLARED_SELECTOR = YES; 641 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 642 | GCC_WARN_UNUSED_FUNCTION = YES; 643 | GCC_WARN_UNUSED_VARIABLE = YES; 644 | MACOSX_DEPLOYMENT_TARGET = 10.14; 645 | MTL_ENABLE_DEBUG_INFO = NO; 646 | MTL_FAST_MATH = YES; 647 | SDKROOT = macosx; 648 | SWIFT_COMPILATION_MODE = wholemodule; 649 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 650 | }; 651 | name = Release; 652 | }; 653 | 518C65B622443C7C00B18604 /* Debug */ = { 654 | isa = XCBuildConfiguration; 655 | buildSettings = { 656 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 657 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 658 | CODE_SIGN_ENTITLEMENTS = "Feed Curator.entitlements"; 659 | CODE_SIGN_STYLE = Automatic; 660 | COMBINE_HIDPI_IMAGES = YES; 661 | DEVELOPMENT_TEAM = SHJK2V3AJG; 662 | ENABLE_HARDENED_RUNTIME = YES; 663 | INFOPLIST_FILE = FeedCurator/Info.plist; 664 | LD_RUNPATH_SEARCH_PATHS = ( 665 | "$(inherited)", 666 | "@executable_path/../Frameworks", 667 | ); 668 | MACOSX_DEPLOYMENT_TARGET = 11.0; 669 | MARKETING_VERSION = 1.4; 670 | PRODUCT_BUNDLE_IDENTIFIER = io.vincode.FeedCurator; 671 | PRODUCT_NAME = "$(TARGET_NAME)"; 672 | SWIFT_VERSION = 4.2; 673 | }; 674 | name = Debug; 675 | }; 676 | 518C65B722443C7C00B18604 /* Release */ = { 677 | isa = XCBuildConfiguration; 678 | buildSettings = { 679 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 680 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 681 | CODE_SIGN_ENTITLEMENTS = "Feed Curator.entitlements"; 682 | CODE_SIGN_STYLE = Automatic; 683 | COMBINE_HIDPI_IMAGES = YES; 684 | DEVELOPMENT_TEAM = SHJK2V3AJG; 685 | ENABLE_HARDENED_RUNTIME = YES; 686 | INFOPLIST_FILE = FeedCurator/Info.plist; 687 | LD_RUNPATH_SEARCH_PATHS = ( 688 | "$(inherited)", 689 | "@executable_path/../Frameworks", 690 | ); 691 | MACOSX_DEPLOYMENT_TARGET = 11.0; 692 | MARKETING_VERSION = 1.4; 693 | PRODUCT_BUNDLE_IDENTIFIER = io.vincode.FeedCurator; 694 | PRODUCT_NAME = "$(TARGET_NAME)"; 695 | SWIFT_VERSION = 4.2; 696 | }; 697 | name = Release; 698 | }; 699 | /* End XCBuildConfiguration section */ 700 | 701 | /* Begin XCConfigurationList section */ 702 | 518C659E22443C7B00B18604 /* Build configuration list for PBXProject "Feed Curator" */ = { 703 | isa = XCConfigurationList; 704 | buildConfigurations = ( 705 | 518C65B322443C7C00B18604 /* Debug */, 706 | 518C65B422443C7C00B18604 /* Release */, 707 | ); 708 | defaultConfigurationIsVisible = 0; 709 | defaultConfigurationName = Release; 710 | }; 711 | 518C65B522443C7C00B18604 /* Build configuration list for PBXNativeTarget "Feed Curator" */ = { 712 | isa = XCConfigurationList; 713 | buildConfigurations = ( 714 | 518C65B622443C7C00B18604 /* Debug */, 715 | 518C65B722443C7C00B18604 /* Release */, 716 | ); 717 | defaultConfigurationIsVisible = 0; 718 | defaultConfigurationName = Release; 719 | }; 720 | /* End XCConfigurationList section */ 721 | }; 722 | rootObject = 518C659B22443C7B00B18604 /* Project object */; 723 | } 724 | -------------------------------------------------------------------------------- /FeedCurator/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | CA 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | Default 537 | 538 | 539 | 540 | 541 | 542 | 543 | Left to Right 544 | 545 | 546 | 547 | 548 | 549 | 550 | Right to Left 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | Default 562 | 563 | 564 | 565 | 566 | 567 | 568 | Left to Right 569 | 570 | 571 | 572 | 573 | 574 | 575 | Right to Left 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 749 | 750 | 751 | 752 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 920 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | --------------------------------------------------------------------------------