Continue march toward non-optional article.status.
This commit is contained in:
parent
6db993075b
commit
b28a849af6
@ -1147,11 +1147,12 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
842E45CE1ED8C308000A8B52 /* AppConstants.swift in Sources */,
|
||||||
|
849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */,
|
||||||
842E45DD1ED8C54B000A8B52 /* Browser.swift in Sources */,
|
842E45DD1ED8C54B000A8B52 /* Browser.swift in Sources */,
|
||||||
849A975B1ED9EB0D007D329B /* ArticleUtilities.swift in Sources */,
|
849A975B1ED9EB0D007D329B /* ArticleUtilities.swift in Sources */,
|
||||||
849A97891ED9ECEF007D329B /* ArticleStyle.swift in Sources */,
|
849A97891ED9ECEF007D329B /* ArticleStyle.swift in Sources */,
|
||||||
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */,
|
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */,
|
||||||
849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */,
|
|
||||||
849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */,
|
849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */,
|
||||||
849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */,
|
849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */,
|
||||||
849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */,
|
849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */,
|
||||||
@ -1167,12 +1168,11 @@
|
|||||||
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
|
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
|
||||||
849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */,
|
849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */,
|
||||||
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
|
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
|
||||||
842E45CE1ED8C308000A8B52 /* AppConstants.swift in Sources */,
|
|
||||||
849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */,
|
|
||||||
849A975C1ED9EB0D007D329B /* DefaultFeedsImporter.swift in Sources */,
|
849A975C1ED9EB0D007D329B /* DefaultFeedsImporter.swift in Sources */,
|
||||||
849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */,
|
849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */,
|
||||||
849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */,
|
849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */,
|
||||||
849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */,
|
849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */,
|
||||||
|
849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */,
|
||||||
842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */,
|
842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */,
|
||||||
849A976D1ED9EBC8007D329B /* TimelineTableView.swift in Sources */,
|
849A976D1ED9EBC8007D329B /* TimelineTableView.swift in Sources */,
|
||||||
849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */,
|
849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */,
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>BuildSystemType</key>
|
||||||
|
<string>Latest</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -18,12 +18,26 @@ extension Notification.Name {
|
|||||||
static let AppNavigationKeyPressed = Notification.Name("AppNavigationKeyPressedNotification")
|
static let AppNavigationKeyPressed = Notification.Name("AppNavigationKeyPressedNotification")
|
||||||
}
|
}
|
||||||
|
|
||||||
let viewKey = "view"
|
struct AppUserInfoKey {
|
||||||
let nodeKey = "node"
|
|
||||||
let objectsKey = "objects"
|
static let view = "view"
|
||||||
let articleKey = "article"
|
static let node = "node"
|
||||||
|
static let objects = "objects"
|
||||||
|
static let article = "article"
|
||||||
|
static let articles = "articles"
|
||||||
|
static let articleStatus = "status"
|
||||||
|
static let appNavigation = "key"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppDefaultsKey {
|
||||||
|
|
||||||
|
static let firstRunDate = "firstRunDate"
|
||||||
|
|
||||||
|
static let sidebarFontSize = "sidebarFontSize"
|
||||||
|
static let timelineFontSize = "timelineFontSize"
|
||||||
|
static let detailFontSize = "detailFontSize"
|
||||||
|
|
||||||
|
static let openInBrowserInBackground = "openInBrowserInBackground"
|
||||||
|
}
|
||||||
|
|
||||||
let articlesKey = "articles"
|
|
||||||
let articleStatusKey = "statusKey"
|
|
||||||
|
|
||||||
let appNavigationKey = "keyKey"
|
|
||||||
|
@ -41,7 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
|||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: NSApplicationDelegate
|
// MARK: - NSApplicationDelegate
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ note: Notification) {
|
func applicationDidFinishLaunching(_ note: Notification) {
|
||||||
|
|
||||||
@ -51,11 +51,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
|||||||
|
|
||||||
let _ = AccountManager.sharedInstance
|
let _ = AccountManager.sharedInstance
|
||||||
|
|
||||||
let kFirstRunDateKey = "firstRun"
|
|
||||||
var isFirstRun = false
|
var isFirstRun = false
|
||||||
if UserDefaults.standard.object(forKey: kFirstRunDateKey) == nil {
|
if UserDefaults.standard.object(forKey: AppDefaultsKey.firstRunDate) == nil {
|
||||||
isFirstRun = true
|
isFirstRun = true
|
||||||
UserDefaults.standard.set(Date(), forKey: kFirstRunDateKey)
|
UserDefaults.standard.set(Date(), forKey: AppDefaultsKey.firstRunDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
importDefaultFeedsIfNeeded(isFirstRun, account: AccountManager.sharedInstance.localAccount)
|
importDefaultFeedsIfNeeded(isFirstRun, account: AccountManager.sharedInstance.localAccount)
|
||||||
@ -224,7 +223,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
|||||||
panel.allowsOtherFileTypes = false
|
panel.allowsOtherFileTypes = false
|
||||||
|
|
||||||
let result = panel.runModal()
|
let result = panel.runModal()
|
||||||
if result == NSFileHandlingPanelOKButton {
|
if result == NSApplication.ModalResponse.OK {
|
||||||
if let url = panel.url {
|
if let url = panel.url {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.parseAndImportOPML(url, AccountManager.sharedInstance.localAccount)
|
self.parseAndImportOPML(url, AccountManager.sharedInstance.localAccount)
|
||||||
@ -250,15 +249,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
|||||||
panel.nameFieldStringValue = "MySubscriptions.opml"
|
panel.nameFieldStringValue = "MySubscriptions.opml"
|
||||||
|
|
||||||
let result = panel.runModal()
|
let result = panel.runModal()
|
||||||
if result == NSFileHandlingPanelOKButton {
|
if result.rawValue == NSFileHandlingPanelOKButton {
|
||||||
if let url = panel.url {
|
if let url = panel.url {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let opmlString = AccountManager.sharedInstance.localAccount.opmlString(indentLevel: 0)
|
let opmlString = AccountManager.sharedInstance.localAccount.OPMLString(indentLevel: 0)
|
||||||
do {
|
do {
|
||||||
try opmlString.write(to: url, atomically: true, encoding: String.Encoding.utf8)
|
try opmlString.write(to: url, atomically: true, encoding: String.Encoding.utf8)
|
||||||
}
|
}
|
||||||
catch let error as NSError {
|
catch let error as NSError {
|
||||||
NSApplication.shared().presentError(error)
|
NSApplication.shared.presentError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -271,7 +270,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
|||||||
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")!
|
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")!
|
||||||
let urlString = "mailto:support@ranchero.com?subject=I%20need%20help%20with%20\(escapedAppName)%20\(version)&body=I%20ran%20into%20a%20problem:%20"
|
let urlString = "mailto:support@ranchero.com?subject=I%20need%20help%20with%20\(escapedAppName)%20\(version)&body=I%20ran%20into%20a%20problem:%20"
|
||||||
if let url = URL(string: urlString) {
|
if let url = URL(string: urlString) {
|
||||||
NSWorkspace.shared().open(url)
|
NSWorkspace.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,17 +320,17 @@ private extension AppDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let parserData = ParserData(data: opmlData, urlString: url.absoluteString)
|
let parserData = ParserData(url: url.absoluteString, data: opmlData)
|
||||||
RSParseOPML(xmlData) { (opmlDocument, error) in
|
RSParseOPML(parserData) { (opmlDocument, error) in
|
||||||
|
|
||||||
if let error = error {
|
if let error = error {
|
||||||
NSApplication.shared().presentError(error)
|
NSApplication.shared.presentError(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let opmlDocument = opmlDocument {
|
if let opmlDocument = opmlDocument {
|
||||||
account.importOPML(opmlDocument)
|
account.importOPML(opmlDocument)
|
||||||
// account.refreshAll()
|
account.refreshAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
|||||||
|
|
||||||
@objc func appNavigationKeyPressed(_ note: Notification) {
|
@objc func appNavigationKeyPressed(_ note: Notification) {
|
||||||
|
|
||||||
guard let key = note.userInfo?[appNavigationKey] as? Int else {
|
guard let key = note.userInfo?[AppKey.appNavigation] as? Int else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let contentView = window?.contentView, let view = note.object as? NSView, view.isDescendant(of: contentView) else {
|
guard let contentView = window?.contentView, let view = note.object as? NSView, view.isDescendant(of: contentView) else {
|
||||||
|
@ -177,9 +177,9 @@ private extension SidebarViewController {
|
|||||||
|
|
||||||
var userInfo = [AnyHashable: Any]()
|
var userInfo = [AnyHashable: Any]()
|
||||||
if let selectedObjects = selectedObjects {
|
if let selectedObjects = selectedObjects {
|
||||||
userInfo[objectsKey] = selectedObjects
|
userInfo[AppKey.objects] = selectedObjects
|
||||||
}
|
}
|
||||||
userInfo[viewKey] = self.outlineView
|
userInfo[AppKey.view] = self.outlineView
|
||||||
|
|
||||||
NotificationCenter.default.post(name: .SidebarSelectionDidChange, object: self, userInfo: userInfo)
|
NotificationCenter.default.post(name: .SidebarSelectionDidChange, object: self, userInfo: userInfo)
|
||||||
}
|
}
|
||||||
|
@ -56,10 +56,10 @@ final class StatusBarView: NSView {
|
|||||||
|
|
||||||
@objc dynamic func timelineSelectionDidChange(_ note: Notification) {
|
@objc dynamic func timelineSelectionDidChange(_ note: Notification) {
|
||||||
|
|
||||||
let timelineView = note.userInfo?[viewKey] as! NSView
|
let timelineView = note.userInfo?[AppKey.view] as! NSView
|
||||||
|
|
||||||
if timelineView.window! === self.window {
|
if timelineView.window! === self.window {
|
||||||
article = note.userInfo?[articleKey] as? Article
|
article = note.userInfo?[AppKey.article] as? Article
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,7 +414,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
|
|||||||
var fetchedArticles = [Article]()
|
var fetchedArticles = [Article]()
|
||||||
for (accountID, objects) in accountsDictionary {
|
for (accountID, objects) in accountsDictionary {
|
||||||
|
|
||||||
guard let oneAccount = account(with: accountID) else {
|
guard let oneAccount = accountWithID(accountID) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,14 +473,14 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
|
|||||||
|
|
||||||
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
|
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
|
||||||
|
|
||||||
let rowView: TimelineTableRowView = tableView.make(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineRow"), owner: self) as! TimelineTableRowView
|
let rowView: TimelineTableRowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineRow"), owner: self) as! TimelineTableRowView
|
||||||
rowView.cellAppearance = cellAppearance
|
rowView.cellAppearance = cellAppearance
|
||||||
return rowView
|
return rowView
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||||
|
|
||||||
let cell: TimelineTableCellView = tableView.make(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineCell"), owner: self) as! TimelineTableCellView
|
let cell: TimelineTableCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timelineCell"), owner: self) as! TimelineTableCellView
|
||||||
cell.cellAppearance = cellAppearance
|
cell.cellAppearance = cellAppearance
|
||||||
|
|
||||||
if let article = articleAtRow(row) {
|
if let article = articleAtRow(row) {
|
||||||
|
@ -8,15 +8,18 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
let SidebarFontSizeKey = "sidebarFontSize"
|
final class AppDefaults {
|
||||||
let TimelineFontSizeKey = "timelineFontSize"
|
|
||||||
let ArticleFontSizeKey = "articleFontSize"
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
let SidebarFontSizeKVOKey = "values." + SidebarFontSizeKey
|
extension AppDefaultsKey {
|
||||||
let TimelineFontSizeKVOKey = "values." + TimelineFontSizeKey
|
|
||||||
let ArticleFontSizeKVOKey = "values." + ArticleFontSizeKey
|
static let sidebarFontSizeKVO = "values." + sidebarFontSize
|
||||||
|
static let timelineFontSizeKVO = "values." + timelineFontSize
|
||||||
let OpenInBrowserInBackgroundKey = "openInBrowserInBackground"
|
static let detailFontSizeKVO = "values." + detailFontSize
|
||||||
|
}
|
||||||
|
|
||||||
enum FontSize: Int {
|
enum FontSize: Int {
|
||||||
case small = 0
|
case small = 0
|
||||||
@ -30,7 +33,7 @@ private let largestFontSizeRawValue = FontSize.veryLarge.rawValue
|
|||||||
|
|
||||||
func registerDefaults() {
|
func registerDefaults() {
|
||||||
|
|
||||||
let defaults = [SidebarFontSizeKey: FontSize.medium.rawValue, TimelineFontSizeKey: FontSize.medium.rawValue, ArticleFontSizeKey: FontSize.medium.rawValue]
|
let defaults = [AppDefaultsKey.sidebarFontSize: FontSize.medium.rawValue, AppDefaultsKey.timelineFontSize: FontSize.medium.rawValue, AppDefaultsKey.detailFontSize, FontSize.medium.rawValue]
|
||||||
|
|
||||||
UserDefaults.standard.register(defaults: defaults)
|
UserDefaults.standard.register(defaults: defaults)
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,16 @@ import Cocoa
|
|||||||
|
|
||||||
private struct PreferencesToolbarItemSpec {
|
private struct PreferencesToolbarItemSpec {
|
||||||
|
|
||||||
let identifier: String // Toolbar item identifier and view controller identifier in storyboard
|
let identifier: NSToolbarItem.Identifier
|
||||||
let name: String
|
let name: String
|
||||||
let imageName: String
|
let imageName: NSImage.Name
|
||||||
|
|
||||||
|
init(identifierRawValue: String, name: String, imageName: NSImage.Name) {
|
||||||
|
|
||||||
|
self.identifier = NSToolbarItem.Identifier(rawValue: identifierRawValue)
|
||||||
|
self.name = name
|
||||||
|
self.imageName = imageName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let toolbarItemIdentifierGeneral = "General"
|
private let toolbarItemIdentifierGeneral = "General"
|
||||||
@ -23,14 +30,14 @@ class PreferencesWindowController : NSWindowController, NSToolbarDelegate {
|
|||||||
fileprivate var viewControllers = [String: NSViewController]()
|
fileprivate var viewControllers = [String: NSViewController]()
|
||||||
fileprivate let toolbarItemSpecs: [PreferencesToolbarItemSpec] = {
|
fileprivate let toolbarItemSpecs: [PreferencesToolbarItemSpec] = {
|
||||||
var specs = [PreferencesToolbarItemSpec]()
|
var specs = [PreferencesToolbarItemSpec]()
|
||||||
specs += [PreferencesToolbarItemSpec(identifier: toolbarItemIdentifierGeneral, name: NSLocalizedString("General", comment: "Preferences"), imageName: NSImageNamePreferencesGeneral)]
|
specs += [PreferencesToolbarItemSpec(identifierRawValue: toolbarItemIdentifierGeneral, name: NSLocalizedString("General", comment: "Preferences"), imageName: NSImage.Name.preferencesGeneral)]
|
||||||
return specs
|
return specs
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
||||||
override func windowDidLoad() {
|
override func windowDidLoad() {
|
||||||
|
|
||||||
let toolbar = NSToolbar(identifier: "PreferencesToolbar")
|
let toolbar = NSToolbar(identifier: NSToolbar.Identifier(rawValue: "PreferencesToolbar"))
|
||||||
toolbar.delegate = self
|
toolbar.delegate = self
|
||||||
toolbar.autosavesConfiguration = false
|
toolbar.autosavesConfiguration = false
|
||||||
toolbar.allowsUserCustomization = false
|
toolbar.allowsUserCustomization = false
|
||||||
@ -70,18 +77,18 @@ class PreferencesWindowController : NSWindowController, NSToolbarDelegate {
|
|||||||
return toolbarItem
|
return toolbarItem
|
||||||
}
|
}
|
||||||
|
|
||||||
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
|
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||||
|
|
||||||
return toolbarItemSpecs.map { $0.identifier }
|
return toolbarItemSpecs.map { $0.identifier }
|
||||||
}
|
}
|
||||||
|
|
||||||
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
|
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||||
|
|
||||||
return toolbarDefaultItemIdentifiers(toolbar)
|
return toolbarDefaultItemIdentifiers(toolbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
|
func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||||
|
|
||||||
return toolbarDefaultItemIdentifiers(toolbar)
|
return toolbarDefaultItemIdentifiers(toolbar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,11 @@ public final class Account: DisplayNameProvider, Hashable {
|
|||||||
return nil //TODO
|
return nil //TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func importOPML(_ opmlDocument: RSOPMLDocument) {
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Equatable
|
// MARK: - Equatable
|
||||||
|
|
||||||
public class func ==(lhs: Account, rhs: Account) -> Bool {
|
public class func ==(lhs: Account, rhs: Account) -> Bool {
|
||||||
|
@ -98,7 +98,7 @@ public final class AccountManager: UnreadCountProvider {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func anyAccountHasFeedWithURL(_ urlString: String) -> Bool {
|
public func anyAccountHasFeedWithURL(_ urlString: String) -> Bool {
|
||||||
|
|
||||||
for account in accounts {
|
for account in accounts {
|
||||||
if let _ = account.existingFeed(withURL: urlString) {
|
if let _ = account.existingFeed(withURL: urlString) {
|
||||||
@ -191,7 +191,7 @@ private func accountFilePathWithFolder(_ folderPath: String) -> String {
|
|||||||
return NSString(string: folderPath).appendingPathComponent(accountDataFileName)
|
return NSString(string: folderPath).appendingPathComponent(accountDataFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func account(with accountID: String) -> Account? {
|
public func accountWithID(_ accountID: String) -> Account? {
|
||||||
|
|
||||||
// Shortcut.
|
// Shortcut.
|
||||||
return AccountManager.sharedInstance.existingAccount(with: accountID)
|
return AccountManager.sharedInstance.existingAccount(with: accountID)
|
||||||
|
@ -13,7 +13,7 @@ public extension Article {
|
|||||||
|
|
||||||
var account: Account? {
|
var account: Account? {
|
||||||
get {
|
get {
|
||||||
return account(with: accountID)
|
return accountWithID(accountID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ public extension Feed {
|
|||||||
|
|
||||||
var account: Account? {
|
var account: Account? {
|
||||||
get {
|
get {
|
||||||
return account(with: accountID)
|
return accountWithID(accountID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ public final class Folder: DisplayNameProvider, UnreadCountProvider {
|
|||||||
|
|
||||||
public var account: Account? {
|
public var account: Account? {
|
||||||
get {
|
get {
|
||||||
return account(with: accountID)
|
return accountWithID(accountID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,12 +56,17 @@ public struct Article: Hashable {
|
|||||||
self.articleID = articleID
|
self.articleID = articleID
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
self.articleID = databaseIDWithString("\(feedID) \(uniqueID)")
|
self.articleID = Article.calculatedArticleID(feedID: feedID, uniqueID: uniqueID)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.hashValue = accountID.hashValue ^ self.articleID.hashValue
|
self.hashValue = accountID.hashValue ^ self.articleID.hashValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func calculatedArticleID(feedID: String, uniqueID: String) -> String {
|
||||||
|
|
||||||
|
return databaseIDWithString("\(feedID) \(uniqueID)")
|
||||||
|
}
|
||||||
|
|
||||||
public static func ==(lhs: Article, rhs: Article) -> Bool {
|
public static func ==(lhs: Article, rhs: Article) -> Bool {
|
||||||
|
|
||||||
return lhs.hashValue == rhs.hashValue && lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.feedID == rhs.feedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.url == rhs.url && lhs.externalURL == rhs.externalURL && lhs.summary == rhs.summary && lhs.imageURL == rhs.imageURL && lhs.bannerImageURL == rhs.bannerImageURL && lhs.datePublished == rhs.datePublished && lhs.authors == rhs.authors && lhs.tags == rhs.tags && lhs.attachments == rhs.attachments
|
return lhs.hashValue == rhs.hashValue && lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.feedID == rhs.feedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.url == rhs.url && lhs.externalURL == rhs.externalURL && lhs.summary == rhs.summary && lhs.imageURL == rhs.imageURL && lhs.bannerImageURL == rhs.bannerImageURL && lhs.datePublished == rhs.datePublished && lhs.authors == rhs.authors && lhs.tags == rhs.tags && lhs.attachments == rhs.attachments
|
||||||
|
@ -16,7 +16,7 @@ import RSCore
|
|||||||
private var databaseIDCache = [String: String]()
|
private var databaseIDCache = [String: String]()
|
||||||
private var databaseIDCacheLock = os_unfair_lock_s()
|
private var databaseIDCacheLock = os_unfair_lock_s()
|
||||||
|
|
||||||
func databaseIDWithString(_ s: String) -> String {
|
public func databaseIDWithString(_ s: String) -> String {
|
||||||
|
|
||||||
os_unfair_lock_lock(&databaseIDCacheLock)
|
os_unfair_lock_lock(&databaseIDCacheLock)
|
||||||
defer {
|
defer {
|
||||||
|
@ -85,8 +85,8 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Create incoming articles with parsedItems.
|
// 1. Ensure statuses for all the incoming articles.
|
||||||
// 2. Ensure statuses for all the incoming articles.
|
// 2. Create incoming articles with parsedItems.
|
||||||
// 3. Ignore incoming articles that are userDeleted || (!starred and really old)
|
// 3. Ignore incoming articles that are userDeleted || (!starred and really old)
|
||||||
// 4. Fetch all articles for the feed.
|
// 4. Fetch all articles for the feed.
|
||||||
// 5. Create array of Articles not in database and save them.
|
// 5. Create array of Articles not in database and save them.
|
||||||
@ -94,19 +94,32 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
// 7. Call back with new and updated Articles.
|
// 7. Call back with new and updated Articles.
|
||||||
|
|
||||||
let feedID = feed.feedID
|
let feedID = feed.feedID
|
||||||
|
let articleIDs = Set(parsedFeed.items.map { $0.articleID })
|
||||||
|
|
||||||
self.queue.run { (database) in
|
self.queue.update { (database) in
|
||||||
|
|
||||||
// This doesn’t hit the database, but it should be done on the database queue.
|
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, database) //1
|
||||||
let allIncomingArticles = Article.articlesWithParsedItems(parsedFeed.items, self.accountID, feedID) //1
|
assert(statusesDictionary.count == articleIDs.count)
|
||||||
|
|
||||||
|
let allIncomingArticles = Article.articlesWithParsedItems(parsedFeed.items, self.accountID, feedID, statusesDictionary) //2
|
||||||
if allIncomingArticles.isEmpty {
|
if allIncomingArticles.isEmpty {
|
||||||
self.callUpdateArticlesCompletionBlock(nil, nil, completion)
|
self.callUpdateArticlesCompletionBlock(nil, nil, completion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
let incomingArticles = self.filterIncomingArticles(allIncomingArticles, statusesDictionary) //3
|
||||||
self.ensureStatusesAndSaveArticles(allIncomingArticles, feedID, completion) //2-7
|
if incomingArticles.isEmpty {
|
||||||
|
self.callUpdateArticlesCompletionBlock(nil, nil, completion)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database: database) //4
|
||||||
|
let fetchedArticlesDictionary = fetchedArticles.dictionary()
|
||||||
|
|
||||||
|
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
|
||||||
|
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
|
||||||
|
|
||||||
|
self.callUpdateArticlesCompletionBlock(newArticles, updatedArticles, completion) //7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,9 +162,7 @@ private extension ArticlesTable {
|
|||||||
// Then fetch the related objects, given the set of articleIDs.
|
// Then fetch the related objects, given the set of articleIDs.
|
||||||
// Then create set of Articles *with* related objects and return it.
|
// Then create set of Articles *with* related objects and return it.
|
||||||
|
|
||||||
let (stubArticles, statuses) = stubArticlesAndStatuses(with: resultSet)
|
let stubArticles = makeStubArticles(with: resultSet)
|
||||||
|
|
||||||
statusesTable.addIfNotCached(statuses)
|
|
||||||
if stubArticles.isEmpty {
|
if stubArticles.isEmpty {
|
||||||
return stubArticles
|
return stubArticles
|
||||||
}
|
}
|
||||||
@ -173,25 +184,25 @@ private extension ArticlesTable {
|
|||||||
return articles
|
return articles
|
||||||
}
|
}
|
||||||
|
|
||||||
func stubArticlesAndStatuses(with resultSet: FMResultSet) -> (Set<Article>, Set<ArticleStatus>) {
|
func makeStubArticles(with resultSet: FMResultSet) -> Set<Article> {
|
||||||
|
|
||||||
var stubArticles = Set<Article>()
|
var stubArticles = Set<Article>()
|
||||||
var statuses = Set<ArticleStatus>()
|
|
||||||
|
|
||||||
// Note: the resultSet is a result of a JOIN query with the statuses table,
|
// Note: the resultSet is a result of a JOIN query with the statuses table,
|
||||||
// so we can get the statuses at the same time and avoid additional database lookups.
|
// so we can get the statuses at the same time and avoid additional database lookups.
|
||||||
|
|
||||||
while resultSet.next() {
|
while resultSet.next() {
|
||||||
if let stubArticle = Article(row: resultSet, accountID: accountID) {
|
guard let status = statusesTable.statusWithRow(resultSet) else {
|
||||||
stubArticles.insert(stubArticle)
|
assertionFailure("Expected status.")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if let status = statusesTable.statusWithRow(resultSet) {
|
if let stubArticle = Article(row: resultSet, accountID: accountID, status: status) {
|
||||||
statuses.insert(status)
|
stubArticles.insert(stubArticle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resultSet.close()
|
resultSet.close()
|
||||||
|
|
||||||
return (stubArticles, statuses)
|
return stubArticles
|
||||||
}
|
}
|
||||||
|
|
||||||
func articleWithAttachedRelatedObjects(_ stubArticle: Article, _ authorsMap: RelatedObjectsMap?, _ attachmentsMap: RelatedObjectsMap?, _ tagsMap: RelatedObjectsMap?) -> Article {
|
func articleWithAttachedRelatedObjects(_ stubArticle: Article, _ authorsMap: RelatedObjectsMap?, _ attachmentsMap: RelatedObjectsMap?, _ tagsMap: RelatedObjectsMap?) -> Article {
|
||||||
@ -263,32 +274,6 @@ private extension ArticlesTable {
|
|||||||
|
|
||||||
// MARK: Saving Parsed Items
|
// MARK: Saving Parsed Items
|
||||||
|
|
||||||
private func ensureStatusesAndSaveArticles(_ allIncomingArticles: Set<Article>, _ feedID: String, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
|
||||||
|
|
||||||
statusesTable.ensureStatusesForArticleIDs(allIncomingArticles.articleIDs()) { (statusesDictionary) in // 2
|
|
||||||
|
|
||||||
self.queue.update{ (database) in
|
|
||||||
self.saveArticlesWithDatabase(allIncomingArticles, statusesDictionary, feedID, database, completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveArticlesWithDatabase(_ allIncomingArticles: Set<Article>, _ statusesDictionary: [String: ArticleStatus], _ feedID: String, _ database: FMDatabase, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { // 3-7
|
|
||||||
|
|
||||||
let incomingArticles = filterIncomingArticles(allIncomingArticles, statusesDictionary) //3
|
|
||||||
if incomingArticles.isEmpty {
|
|
||||||
callUpdateArticlesCompletionBlock(nil, nil, completion)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let fetchedArticles = fetchArticlesForFeedID(feedID, withLimits: false, database: database) //4
|
|
||||||
let fetchedArticlesDictionary = fetchedArticles.dictionary()
|
|
||||||
|
|
||||||
let newArticles = findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
|
|
||||||
let updatedArticles = findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
|
|
||||||
|
|
||||||
callUpdateArticlesCompletionBlock(newArticles, updatedArticles, completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func callUpdateArticlesCompletionBlock(_ newArticles: Set<Article>?, _ updatedArticles: Set<Article>?, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
func callUpdateArticlesCompletionBlock(_ newArticles: Set<Article>?, _ updatedArticles: Set<Article>?, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */; };
|
840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */; };
|
||||||
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */; };
|
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */; };
|
||||||
84288A021F6A3D8000395871 /* RelatedObjectsMap+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */; };
|
84288A021F6A3D8000395871 /* RelatedObjectsMap+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */; };
|
||||||
|
843702C31F70D15D00B18807 /* ParsedArticle+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843702C21F70D15D00B18807 /* ParsedArticle+Database.swift */; };
|
||||||
843CB9961F34174100EE6581 /* Author+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F901F1810DD00D8E682 /* Author+Database.swift */; };
|
843CB9961F34174100EE6581 /* Author+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F901F1810DD00D8E682 /* Author+Database.swift */; };
|
||||||
844BEE411F0AB3AB004AB7CD /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE371F0AB3AA004AB7CD /* Database.framework */; };
|
844BEE411F0AB3AB004AB7CD /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE371F0AB3AA004AB7CD /* Database.framework */; };
|
||||||
844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; };
|
844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; };
|
||||||
@ -116,6 +117,7 @@
|
|||||||
840405CE1F1A963700DF0296 /* AttachmentsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsTable.swift; sourceTree = "<group>"; };
|
840405CE1F1A963700DF0296 /* AttachmentsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsTable.swift; sourceTree = "<group>"; };
|
||||||
842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseObject+Database.swift"; sourceTree = "<group>"; };
|
842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseObject+Database.swift"; sourceTree = "<group>"; };
|
||||||
84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelatedObjectsMap+Database.swift"; sourceTree = "<group>"; };
|
84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelatedObjectsMap+Database.swift"; sourceTree = "<group>"; };
|
||||||
|
843702C21F70D15D00B18807 /* ParsedArticle+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "ParsedArticle+Database.swift"; path = "Extensions/ParsedArticle+Database.swift"; sourceTree = "<group>"; };
|
||||||
844BEE371F0AB3AA004AB7CD /* Database.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Database.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
844BEE371F0AB3AA004AB7CD /* Database.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Database.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
844BEE401F0AB3AB004AB7CD /* DatabaseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatabaseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
844BEE401F0AB3AB004AB7CD /* DatabaseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatabaseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = "<group>"; };
|
844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = "<group>"; };
|
||||||
@ -216,6 +218,7 @@
|
|||||||
children = (
|
children = (
|
||||||
846FB36A1F4A937B00EAB81D /* Feed+Database.swift */,
|
846FB36A1F4A937B00EAB81D /* Feed+Database.swift */,
|
||||||
845580751F0AF670003CCFA1 /* Article+Database.swift */,
|
845580751F0AF670003CCFA1 /* Article+Database.swift */,
|
||||||
|
843702C21F70D15D00B18807 /* ParsedArticle+Database.swift */,
|
||||||
845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */,
|
845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */,
|
||||||
84F20F901F1810DD00D8E682 /* Author+Database.swift */,
|
84F20F901F1810DD00D8E682 /* Author+Database.swift */,
|
||||||
8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */,
|
8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */,
|
||||||
@ -483,6 +486,7 @@
|
|||||||
840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */,
|
840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */,
|
||||||
84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */,
|
84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */,
|
||||||
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */,
|
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */,
|
||||||
|
843702C31F70D15D00B18807 /* ParsedArticle+Database.swift in Sources */,
|
||||||
84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */,
|
84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */,
|
||||||
84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */,
|
84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */,
|
||||||
84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */,
|
84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */,
|
||||||
|
@ -13,7 +13,7 @@ import RSParser
|
|||||||
|
|
||||||
extension Article {
|
extension Article {
|
||||||
|
|
||||||
init?(row: FMResultSet, accountID: String, authors: Set<Author>? = nil, attachments: Set<Attachment>? = nil, tags: Set<String>? = nil) {
|
init?(row: FMResultSet, accountID: String, authors: Set<Author>? = nil, attachments: Set<Attachment>? = nil, tags: Set<String>? = nil, status: ArticleStatus) {
|
||||||
|
|
||||||
guard let feedID = row.string(forColumn: DatabaseKey.feedID) else {
|
guard let feedID = row.string(forColumn: DatabaseKey.feedID) else {
|
||||||
return nil
|
return nil
|
||||||
@ -35,15 +35,15 @@ extension Article {
|
|||||||
let dateModified = row.date(forColumn: DatabaseKey.dateModified)
|
let dateModified = row.date(forColumn: DatabaseKey.dateModified)
|
||||||
let accountInfo: AccountInfo? = nil // TODO
|
let accountInfo: AccountInfo? = nil // TODO
|
||||||
|
|
||||||
self.init(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo)
|
self.init(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo, status: status)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(parsedItem: ParsedItem, accountID: String, feedID: String) {
|
init(parsedItem: ParsedItem, accountID: String, feedID: String, status: ArticleStatus) {
|
||||||
|
|
||||||
let authors = Author.authorsWithParsedAuthors(parsedItem.authors)
|
let authors = Author.authorsWithParsedAuthors(parsedItem.authors)
|
||||||
let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments)
|
let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments)
|
||||||
|
|
||||||
self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, tags: parsedItem.tags, attachments: attachments, accountInfo: nil)
|
self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, tags: parsedItem.tags, attachments: attachments, accountInfo: nil, status: status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func articleByAttaching(_ authors: Set<Author>?, _ attachments: Set<Attachment>?, _ tags: Set<String>?) -> Article {
|
func articleByAttaching(_ authors: Set<Author>?, _ attachments: Set<Attachment>?, _ tags: Set<String>?) -> Article {
|
||||||
@ -52,7 +52,7 @@ extension Article {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
return Article(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo)
|
return Article(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo, status: status)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath<Article,String?>, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) {
|
private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath<Article,String?>, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) {
|
||||||
@ -108,9 +108,9 @@ extension Article {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
static func articlesWithParsedItems(_ parsedItems: Set<ParsedItem>, _ accountID: String, _ feedID: String) -> Set<Article> {
|
static func articlesWithParsedItems(_ parsedItems: Set<ParsedItem>, _ accountID: String, _ feedID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
|
||||||
|
|
||||||
return Set(parsedItems.map{ Article(parsedItem: $0, accountID: accountID, feedID: feedID) })
|
return Set(parsedItems.map{ Article(parsedItem: $0, accountID: accountID, feedID: feedID, status: statusesDictionary[$0.articleID]!) })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
24
Frameworks/Database/Extensions/ParsedArticle+Database.swift
Normal file
24
Frameworks/Database/Extensions/ParsedArticle+Database.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// ParsedArticle+Database.swift
|
||||||
|
// Database
|
||||||
|
//
|
||||||
|
// Created by Brent Simmons on 9/18/17.
|
||||||
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import RSParser
|
||||||
|
import Data
|
||||||
|
|
||||||
|
extension ParsedItem {
|
||||||
|
|
||||||
|
var articleID: String {
|
||||||
|
get {
|
||||||
|
if let s = syncServiceID {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
// Must be same calculation as for Article.
|
||||||
|
return Article.calculatedArticleID(feedID: feedURL, uniqueID: uniqueID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,8 +15,6 @@ import Data
|
|||||||
//
|
//
|
||||||
// CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, userDeleted BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0, accountInfo BLOB);
|
// CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, userDeleted BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0, accountInfo BLOB);
|
||||||
|
|
||||||
typealias StatusesCompletionBlock = ([String: ArticleStatus]) -> Void // [articleID: Status]
|
|
||||||
|
|
||||||
final class StatusesTable: DatabaseTable {
|
final class StatusesTable: DatabaseTable {
|
||||||
|
|
||||||
let name = DatabaseTableName.statuses
|
let name = DatabaseTableName.statuses
|
||||||
@ -28,56 +26,26 @@ final class StatusesTable: DatabaseTable {
|
|||||||
self.queue = queue
|
self.queue = queue
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Cache
|
|
||||||
|
|
||||||
// func cachedStatus(for articleID: String) -> ArticleStatus? {
|
|
||||||
//
|
|
||||||
// assert(Thread.isMainThread)
|
|
||||||
// assert(cache[articleID] != nil)
|
|
||||||
// return cache[articleID]
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// func cachedStatuses(for articleIDs: Set<String>) -> Set<ArticleStatus> {
|
|
||||||
//
|
|
||||||
// assert(Thread.isMainThread)
|
|
||||||
//
|
|
||||||
// var statuses = Set<ArticleStatus>()
|
|
||||||
// for articleID in articleIDs {
|
|
||||||
// if let articleStatus = cache[articleID] {
|
|
||||||
// statuses.insert(articleStatus)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return statuses
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: Creating/Updating
|
// MARK: Creating/Updating
|
||||||
|
|
||||||
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ completion: @escaping StatusesCompletionBlock) {
|
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) -> [String: ArticleStatus] {
|
||||||
|
|
||||||
// Adds them to the cache if not cached.
|
|
||||||
|
|
||||||
assert(Thread.isMainThread)
|
|
||||||
|
|
||||||
// Check cache.
|
// Check cache.
|
||||||
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
|
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
|
||||||
if articleIDsMissingCachedStatus.isEmpty {
|
if articleIDsMissingCachedStatus.isEmpty {
|
||||||
completion(statusesDictionary(articleIDs))
|
return statusesDictionary(articleIDs)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check database.
|
// Check database.
|
||||||
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus) {
|
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus, database)
|
||||||
|
|
||||||
let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs)
|
let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs)
|
||||||
if !articleIDsNeedingStatus.isEmpty {
|
if !articleIDsNeedingStatus.isEmpty {
|
||||||
// Create new statuses.
|
// Create new statuses.
|
||||||
self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus)
|
self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, database)
|
||||||
}
|
|
||||||
|
|
||||||
completion(self.statusesDictionary(articleIDs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return statusesDictionary(articleIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Marking
|
// MARK: Marking
|
||||||
@ -100,9 +68,10 @@ final class StatusesTable: DatabaseTable {
|
|||||||
if updatedStatuses.isEmpty {
|
if updatedStatuses.isEmpty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let articleIDs = updatedStatuses.articleIDs()
|
||||||
|
|
||||||
queue.update { (database) in
|
queue.update { (database) in
|
||||||
self.markArticleIDs(updatedStatuses.articleIDs(), statusKey, flag, database)
|
self.markArticleIDs(articleIDs, statusKey, flag, database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,11 +82,17 @@ final class StatusesTable: DatabaseTable {
|
|||||||
guard let articleID = row.string(forColumn: DatabaseKey.articleID) else {
|
guard let articleID = row.string(forColumn: DatabaseKey.articleID) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if let cachedStatus = cache[articleID] {
|
||||||
|
return cachedStatus
|
||||||
|
}
|
||||||
|
|
||||||
guard let dateArrived = row.date(forColumn: DatabaseKey.dateArrived) else {
|
guard let dateArrived = row.date(forColumn: DatabaseKey.dateArrived) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let articleStatus = ArticleStatus(articleID: articleID, dateArrived: dateArrived, row: row)
|
let articleStatus = ArticleStatus(articleID: articleID, dateArrived: dateArrived, row: row)
|
||||||
|
cache.addStatusIfNotCached(articleStatus)
|
||||||
|
|
||||||
return articleStatus
|
return articleStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,13 +105,13 @@ private extension StatusesTable {
|
|||||||
|
|
||||||
func articleIDsWithNoCachedStatus(_ articleIDs: Set<String>) -> Set<String> {
|
func articleIDsWithNoCachedStatus(_ articleIDs: Set<String>) -> Set<String> {
|
||||||
|
|
||||||
assert(Thread.isMainThread)
|
assert(!Thread.isMainThread)
|
||||||
return Set(articleIDs.filter { cache[$0] == nil })
|
return Set(articleIDs.filter { cache[$0] == nil })
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusesDictionary(_ articleIDs: Set<String>) -> [String: ArticleStatus] {
|
func statusesDictionary(_ articleIDs: Set<String>) -> [String: ArticleStatus] {
|
||||||
|
|
||||||
assert(Thread.isMainThread)
|
assert(!Thread.isMainThread)
|
||||||
|
|
||||||
var d = [String: ArticleStatus]()
|
var d = [String: ArticleStatus]()
|
||||||
|
|
||||||
@ -149,75 +124,31 @@ private extension StatusesTable {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func addToCache(_ statuses: Set<ArticleStatus>) {
|
|
||||||
|
|
||||||
// Replacing any already cached statuses.
|
|
||||||
if statuses.isEmpty {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if Thread.isMainThread {
|
|
||||||
self.cache.add(statuses)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.cache.add(statuses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addIfNotCached(_ statuses: Set<ArticleStatus>) {
|
|
||||||
|
|
||||||
if statuses.isEmpty {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if Thread.isMainThread {
|
|
||||||
self.cache.addIfNotCached(statuses)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.cache.addIfNotCached(statuses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Creating
|
// MARK: Creating
|
||||||
|
|
||||||
func saveStatuses(_ statuses: Set<ArticleStatus>) {
|
func saveStatuses(_ statuses: Set<ArticleStatus>, _ database: FMDatabase) {
|
||||||
|
|
||||||
queue.update { (database) in
|
let statusArray = statuses.map { $0.databaseDictionary()! }
|
||||||
let statusArray = statuses.map { $0.databaseDictionary()! }
|
self.insertRows(statusArray, insertType: .orIgnore, in: database)
|
||||||
self.insertRows(statusArray, insertType: .orIgnore, in: database)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>) {
|
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
|
||||||
|
|
||||||
assert(Thread.isMainThread)
|
|
||||||
|
|
||||||
let now = Date()
|
let now = Date()
|
||||||
let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) })
|
let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) })
|
||||||
cache.addIfNotCached(statuses)
|
cache.addIfNotCached(statuses)
|
||||||
|
|
||||||
saveStatuses(statuses)
|
saveStatuses(statuses, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set<String>, _ completion: @escaping RSVoidCompletionBlock) {
|
func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
|
||||||
|
|
||||||
queue.fetch { (database) in
|
guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else {
|
||||||
guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else {
|
return
|
||||||
completion()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let statuses = resultSet.mapToSet(self.statusWithRow)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.cache.addIfNotCached(statuses)
|
|
||||||
completion()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let statuses = resultSet.mapToSet(self.statusWithRow)
|
||||||
|
self.cache.addIfNotCached(statuses)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Marking
|
// MARK: Marking
|
||||||
@ -228,6 +159,8 @@ private extension StatusesTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
private final class StatusCache {
|
private final class StatusCache {
|
||||||
|
|
||||||
// Serial database queue only.
|
// Serial database queue only.
|
||||||
@ -245,6 +178,11 @@ private final class StatusCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addStatusIfNotCached(_ status: ArticleStatus) {
|
||||||
|
|
||||||
|
addIfNotCached(Set([status]))
|
||||||
|
}
|
||||||
|
|
||||||
func addIfNotCached(_ statuses: Set<ArticleStatus>) {
|
func addIfNotCached(_ statuses: Set<ArticleStatus>) {
|
||||||
|
|
||||||
// Does not replace already cached statuses.
|
// Does not replace already cached statuses.
|
||||||
|
@ -12,6 +12,12 @@
|
|||||||
@class ParserData;
|
@class ParserData;
|
||||||
@class RSOPMLDocument;
|
@class RSOPMLDocument;
|
||||||
|
|
||||||
|
typedef void (^OPMLParserCallback)(RSOPMLDocument *opmlDocument, NSError *error);
|
||||||
|
|
||||||
|
// Parses on background thread; calls back on main thread.
|
||||||
|
void RSParseOPML(ParserData *parserData, OPMLParserCallback callback);
|
||||||
|
|
||||||
|
|
||||||
@interface RSOPMLParser: NSObject
|
@interface RSOPMLParser: NSObject
|
||||||
|
|
||||||
+ (RSOPMLDocument *)parseOPMLWithParserData:(ParserData *)parserData error:(NSError **)error;
|
+ (RSOPMLDocument *)parseOPMLWithParserData:(ParserData *)parserData error:(NSError **)error;
|
||||||
|
@ -24,6 +24,20 @@
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
void RSParseOPML(ParserData *parserData, OPMLParserCallback callback) {
|
||||||
|
|
||||||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
|
||||||
|
@autoreleasepool {
|
||||||
|
NSError *error = nil;
|
||||||
|
RSOPMLDocument *opmlDocument = [RSOPMLParser parseOPMLWithParserData:parserData error:&error];
|
||||||
|
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
callback(opmlDocument, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@implementation RSOPMLParser
|
@implementation RSOPMLParser
|
||||||
|
|
||||||
|
21
ToDo.opml
21
ToDo.opml
@ -6,12 +6,12 @@
|
|||||||
</editor> -->
|
</editor> -->
|
||||||
<title>ToDo</title>
|
<title>ToDo</title>
|
||||||
<dateCreated>Tue, 12 Sep 2017 20:15:17 GMT</dateCreated>
|
<dateCreated>Tue, 12 Sep 2017 20:15:17 GMT</dateCreated>
|
||||||
<expansionState>0,18,21,25,30,40,41,43,47,50,52,54,56,65,70</expansionState>
|
<expansionState>0,23,26,30,35,45,46,48,52,55,57,59,61,70,75</expansionState>
|
||||||
<vertScrollState>0</vertScrollState>
|
<vertScrollState>0</vertScrollState>
|
||||||
<windowTop>452</windowTop>
|
<windowTop>207</windowTop>
|
||||||
<windowLeft>543</windowLeft>
|
<windowLeft>30</windowLeft>
|
||||||
<windowRight>1275</windowRight>
|
<windowRight>762</windowRight>
|
||||||
<windowBottom>1211</windowBottom>
|
<windowBottom>966</windowBottom>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<outline text="App">
|
<outline text="App">
|
||||||
@ -26,10 +26,17 @@
|
|||||||
<outline text="NSError.setUserInfoValueProvider"/>
|
<outline text="NSError.setUserInfoValueProvider"/>
|
||||||
<outline text="NSDictionary.sharedKeySet"/>
|
<outline text="NSDictionary.sharedKeySet"/>
|
||||||
<outline text="Periodic refresh"/>
|
<outline text="Periodic refresh"/>
|
||||||
|
<outline text="OPML Import">
|
||||||
|
<outline text="Accessory view in choose-file sheet for choosing account"/>
|
||||||
|
<outline text="Import from URL">
|
||||||
|
<outline text="Sheet with URL text field and account popup"/>
|
||||||
|
<outline text="Download URL"/>
|
||||||
|
<outline text="Progress while downloading URL"/>
|
||||||
|
<outline text="Report error if needed"/>
|
||||||
|
</outline>
|
||||||
|
</outline>
|
||||||
<outline text="Commands">
|
<outline text="Commands">
|
||||||
<outline text="Star"/>
|
<outline text="Star"/>
|
||||||
<outline text="Import OPML from File"/>
|
|
||||||
<outline text="Import OPML from URL"/>
|
|
||||||
<outline text="Toggle sidebar"/>
|
<outline text="Toggle sidebar"/>
|
||||||
</outline>
|
</outline>
|
||||||
<outline text="Main window">
|
<outline text="Main window">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user