Move OPML I/O functions into a separate OPMLFile class
This commit is contained in:
parent
979fcbc013
commit
83476baa5b
|
@ -167,7 +167,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0)
|
||||
|
||||
private var unreadCounts = [String: Int]() // [feedID: Int]
|
||||
private let opmlFilePath: String
|
||||
private lazy var opmlFile: OPMLFile = {
|
||||
OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self)
|
||||
}()
|
||||
|
||||
private var _flattenedFeeds = Set<Feed>()
|
||||
private var flattenedFeedsNeedUpdate = true
|
||||
|
@ -251,8 +253,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
self.type = type
|
||||
self.dataFolder = dataFolder
|
||||
|
||||
self.opmlFilePath = (dataFolder as NSString).appendingPathComponent("Subscriptions.opml")
|
||||
|
||||
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3")
|
||||
self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID)
|
||||
|
||||
|
@ -396,6 +396,44 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
|
||||
}
|
||||
|
||||
func loadOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) {
|
||||
var feedsToAdd = Set<Feed>()
|
||||
|
||||
items.forEach { (item) in
|
||||
|
||||
if let feedSpecifier = item.feedSpecifier {
|
||||
let feed = newFeed(with: feedSpecifier)
|
||||
feedsToAdd.insert(feed)
|
||||
return
|
||||
}
|
||||
|
||||
guard let folderName = item.titleFromAttributes else {
|
||||
// Folder doesn’t have a name, so it won’t be created, and its items will go one level up.
|
||||
if let itemChildren = item.children {
|
||||
loadOPMLItems(itemChildren, parentFolder: parentFolder)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let folder = ensureFolder(with: folderName) {
|
||||
if let itemChildren = item.children {
|
||||
loadOPMLItems(itemChildren, parentFolder: folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let parentFolder = parentFolder {
|
||||
for feed in feedsToAdd {
|
||||
parentFolder.addFeed(feed)
|
||||
}
|
||||
} else {
|
||||
for feed in feedsToAdd {
|
||||
addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||
return delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag)
|
||||
}
|
||||
|
@ -501,19 +539,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
structureDidChange()
|
||||
}
|
||||
|
||||
func loadOPML(_ opmlDocument: RSOPMLDocument) {
|
||||
guard let children = opmlDocument.children else {
|
||||
return
|
||||
}
|
||||
loadOPMLItems(children, parentFolder: nil)
|
||||
structureDidChange()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.refreshAll() { result in }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func updateUnreadCounts(for feeds: Set<Feed>) {
|
||||
if feeds.isEmpty {
|
||||
return
|
||||
|
@ -590,32 +615,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
return database.fetchArticleIDsForStatusesWithoutArticles()
|
||||
}
|
||||
|
||||
public func opmlDocument() -> String {
|
||||
let escapedTitle = nameForDisplay.rs_stringByEscapingSpecialXMLCharacters()
|
||||
let openingText =
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- OPML generated by NetNewsWire -->
|
||||
<opml version="1.1">
|
||||
<head>
|
||||
<title>\(escapedTitle)</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
"""
|
||||
|
||||
let middleText = OPMLString(indentLevel: 0)
|
||||
|
||||
let closingText =
|
||||
"""
|
||||
</body>
|
||||
</opml>
|
||||
"""
|
||||
|
||||
let opml = openingText + middleText + closingText
|
||||
return opml
|
||||
}
|
||||
|
||||
public func unreadCount(for feed: Feed) -> Int {
|
||||
return unreadCounts[feed.feedID] ?? 0
|
||||
}
|
||||
|
@ -773,7 +772,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
|
||||
@objc func saveToDiskIfNeeded() {
|
||||
if dirty && !isDeleted {
|
||||
saveToDisk()
|
||||
dirty = false
|
||||
opmlFile.save()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -967,7 +967,7 @@ private extension Account {
|
|||
func pullObjectsFromDisk() {
|
||||
loadAccountMetadata()
|
||||
loadFeedMetadata()
|
||||
loadOPMLFile(path: opmlFilePath)
|
||||
opmlFile.load()
|
||||
}
|
||||
|
||||
func loadAccountMetadata() {
|
||||
|
@ -991,52 +991,6 @@ private extension Account {
|
|||
feedMetadata.values.forEach { $0.delegate = self }
|
||||
}
|
||||
|
||||
func loadOPMLFile(path: String) {
|
||||
let opmlFileURL = URL(fileURLWithPath: path)
|
||||
var fileData: Data?
|
||||
do {
|
||||
fileData = try Data(contentsOf: opmlFileURL)
|
||||
} catch {
|
||||
// Commented out because it’s not an error on first run.
|
||||
// TODO: make it so we know if it’s first run or not.
|
||||
//NSApplication.shared.presentError(error)
|
||||
return
|
||||
}
|
||||
guard let opmlData = fileData else {
|
||||
return
|
||||
}
|
||||
|
||||
let parserData = ParserData(url: opmlFileURL.absoluteString, data: opmlData)
|
||||
var opmlDocument: RSOPMLDocument?
|
||||
|
||||
do {
|
||||
opmlDocument = try RSOPMLParser.parseOPML(with: parserData)
|
||||
} catch {
|
||||
os_log(.error, log: log, "OPML Import failed: %@.", error.localizedDescription)
|
||||
return
|
||||
}
|
||||
guard let parsedOPML = opmlDocument, let children = parsedOPML.children else {
|
||||
return
|
||||
}
|
||||
|
||||
BatchUpdate.shared.perform {
|
||||
loadOPMLItems(children, parentFolder: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func saveToDisk() {
|
||||
dirty = false
|
||||
|
||||
let opmlDocumentString = opmlDocument()
|
||||
do {
|
||||
let url = URL(fileURLWithPath: opmlFilePath)
|
||||
try opmlDocumentString.write(to: url, atomically: true, encoding: .utf8)
|
||||
}
|
||||
catch let error as NSError {
|
||||
os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func queueSaveFeedMetadataIfNeeded() {
|
||||
Account.saveQueue.add(self, #selector(saveFeedMetadataIfNeeded))
|
||||
}
|
||||
|
@ -1121,44 +1075,6 @@ private extension Account {
|
|||
feedDictionaryNeedsUpdate = false
|
||||
}
|
||||
|
||||
func loadOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) {
|
||||
var feedsToAdd = Set<Feed>()
|
||||
|
||||
items.forEach { (item) in
|
||||
|
||||
if let feedSpecifier = item.feedSpecifier {
|
||||
let feed = newFeed(with: feedSpecifier)
|
||||
feedsToAdd.insert(feed)
|
||||
return
|
||||
}
|
||||
|
||||
guard let folderName = item.titleFromAttributes else {
|
||||
// Folder doesn’t have a name, so it won’t be created, and its items will go one level up.
|
||||
if let itemChildren = item.children {
|
||||
loadOPMLItems(itemChildren, parentFolder: parentFolder)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let folder = ensureFolder(with: folderName) {
|
||||
if let itemChildren = item.children {
|
||||
loadOPMLItems(itemChildren, parentFolder: folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let parentFolder = parentFolder {
|
||||
for feed in feedsToAdd {
|
||||
parentFolder.addFeed(feed)
|
||||
}
|
||||
} else {
|
||||
for feed in feedsToAdd {
|
||||
addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func updateUnreadCount() {
|
||||
if fetchingAllUnreadCounts {
|
||||
return
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71D22835E9800D9D53D /* FeedSpecifier.swift */; };
|
||||
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71E22835E9800D9D53D /* HTMLFeedFinder.swift */; };
|
||||
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */; };
|
||||
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; };
|
||||
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
|
||||
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
|
||||
51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; };
|
||||
|
@ -140,6 +141,7 @@
|
|||
5165D71D22835E9800D9D53D /* FeedSpecifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedSpecifier.swift; sourceTree = "<group>"; };
|
||||
5165D71E22835E9800D9D53D /* HTMLFeedFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLFeedFinder.swift; sourceTree = "<group>"; };
|
||||
5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialFeedDownloader.swift; sourceTree = "<group>"; };
|
||||
5170743B232AEDB500A461A3 /* OPMLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLFile.swift; sourceTree = "<group>"; };
|
||||
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
|
||||
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
|
||||
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
|
||||
|
@ -333,6 +335,7 @@
|
|||
841974241F6DDCE4006346C4 /* AccountDelegate.swift */,
|
||||
51E3EB40229AF61B00645299 /* AccountError.swift */,
|
||||
846E77531F6F00E300A165E2 /* AccountManager.swift */,
|
||||
5170743B232AEDB500A461A3 /* OPMLFile.swift */,
|
||||
84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */,
|
||||
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
|
||||
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
|
||||
|
@ -591,6 +594,7 @@
|
|||
5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */,
|
||||
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */,
|
||||
552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */,
|
||||
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */,
|
||||
552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */,
|
||||
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
|
||||
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
|
||||
|
|
|
@ -77,11 +77,14 @@ final class LocalAccountDelegate: AccountDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
// We use the same mechanism to load local accounts as we do to load the subscription
|
||||
// OPML all accounts.
|
||||
BatchUpdate.shared.perform {
|
||||
account.loadOPML(loadDocument)
|
||||
guard let children = loadDocument.children else {
|
||||
return
|
||||
}
|
||||
|
||||
BatchUpdate.shared.perform {
|
||||
account.loadOPMLItems(children, parentFolder: nil)
|
||||
}
|
||||
|
||||
completion(.success(()))
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
//
|
||||
// OPMLFile.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 9/12/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import RSCore
|
||||
import RSParser
|
||||
|
||||
final class OPMLFile: NSObject, NSFilePresenter {
|
||||
|
||||
|
||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "account")
|
||||
|
||||
private let filename: String
|
||||
private let account: Account
|
||||
private let operationQueue: OperationQueue
|
||||
|
||||
var presentedItemURL: URL? {
|
||||
return URL(string: filename)
|
||||
}
|
||||
|
||||
var presentedItemOperationQueue: OperationQueue {
|
||||
return operationQueue
|
||||
}
|
||||
|
||||
init(filename: String, account: Account) {
|
||||
self.filename = filename
|
||||
self.account = account
|
||||
operationQueue = OperationQueue()
|
||||
operationQueue.maxConcurrentOperationCount = 1
|
||||
}
|
||||
|
||||
func load() {
|
||||
let opmlFileURL = URL(fileURLWithPath: filename)
|
||||
var fileData: Data?
|
||||
do {
|
||||
fileData = try Data(contentsOf: opmlFileURL)
|
||||
} catch {
|
||||
// Commented out because it’s not an error on first run.
|
||||
// TODO: make it so we know if it’s first run or not.
|
||||
//NSApplication.shared.presentError(error)
|
||||
return
|
||||
}
|
||||
guard let opmlData = fileData else {
|
||||
return
|
||||
}
|
||||
|
||||
let parserData = ParserData(url: opmlFileURL.absoluteString, data: opmlData)
|
||||
var opmlDocument: RSOPMLDocument?
|
||||
|
||||
do {
|
||||
opmlDocument = try RSOPMLParser.parseOPML(with: parserData)
|
||||
} catch {
|
||||
os_log(.error, log: log, "OPML Import failed: %@.", error.localizedDescription)
|
||||
return
|
||||
}
|
||||
guard let parsedOPML = opmlDocument, let children = parsedOPML.children else {
|
||||
return
|
||||
}
|
||||
|
||||
BatchUpdate.shared.perform {
|
||||
account.loadOPMLItems(children, parentFolder: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func save() {
|
||||
|
||||
let opmlDocumentString = opmlDocument()
|
||||
do {
|
||||
let url = URL(fileURLWithPath: filename)
|
||||
try opmlDocumentString.write(to: url, atomically: true, encoding: .utf8)
|
||||
} catch let error as NSError {
|
||||
os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension OPMLFile {
|
||||
|
||||
func opmlDocument() -> String {
|
||||
let escapedTitle = account.nameForDisplay.rs_stringByEscapingSpecialXMLCharacters()
|
||||
let openingText =
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- OPML generated by NetNewsWire -->
|
||||
<opml version="1.1">
|
||||
<head>
|
||||
<title>\(escapedTitle)</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
"""
|
||||
|
||||
let middleText = account.OPMLString(indentLevel: 0)
|
||||
|
||||
let closingText =
|
||||
"""
|
||||
</body>
|
||||
</opml>
|
||||
"""
|
||||
|
||||
let opml = openingText + middleText + closingText
|
||||
return opml
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue