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)
|
static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0)
|
||||||
|
|
||||||
private var unreadCounts = [String: Int]() // [feedID: Int]
|
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 _flattenedFeeds = Set<Feed>()
|
||||||
private var flattenedFeedsNeedUpdate = true
|
private var flattenedFeedsNeedUpdate = true
|
||||||
@ -251,8 +253,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
self.type = type
|
self.type = type
|
||||||
self.dataFolder = dataFolder
|
self.dataFolder = dataFolder
|
||||||
|
|
||||||
self.opmlFilePath = (dataFolder as NSString).appendingPathComponent("Subscriptions.opml")
|
|
||||||
|
|
||||||
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3")
|
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3")
|
||||||
self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID)
|
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>? {
|
public func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||||
return delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag)
|
return delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag)
|
||||||
}
|
}
|
||||||
@ -501,19 +539,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
structureDidChange()
|
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>) {
|
public func updateUnreadCounts(for feeds: Set<Feed>) {
|
||||||
if feeds.isEmpty {
|
if feeds.isEmpty {
|
||||||
return
|
return
|
||||||
@ -590,32 +615,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
return database.fetchArticleIDsForStatusesWithoutArticles()
|
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 {
|
public func unreadCount(for feed: Feed) -> Int {
|
||||||
return unreadCounts[feed.feedID] ?? 0
|
return unreadCounts[feed.feedID] ?? 0
|
||||||
}
|
}
|
||||||
@ -773,7 +772,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
|
|
||||||
@objc func saveToDiskIfNeeded() {
|
@objc func saveToDiskIfNeeded() {
|
||||||
if dirty && !isDeleted {
|
if dirty && !isDeleted {
|
||||||
saveToDisk()
|
dirty = false
|
||||||
|
opmlFile.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -967,7 +967,7 @@ private extension Account {
|
|||||||
func pullObjectsFromDisk() {
|
func pullObjectsFromDisk() {
|
||||||
loadAccountMetadata()
|
loadAccountMetadata()
|
||||||
loadFeedMetadata()
|
loadFeedMetadata()
|
||||||
loadOPMLFile(path: opmlFilePath)
|
opmlFile.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAccountMetadata() {
|
func loadAccountMetadata() {
|
||||||
@ -991,52 +991,6 @@ private extension Account {
|
|||||||
feedMetadata.values.forEach { $0.delegate = self }
|
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() {
|
func queueSaveFeedMetadataIfNeeded() {
|
||||||
Account.saveQueue.add(self, #selector(saveFeedMetadataIfNeeded))
|
Account.saveQueue.add(self, #selector(saveFeedMetadataIfNeeded))
|
||||||
}
|
}
|
||||||
@ -1120,44 +1074,6 @@ private extension Account {
|
|||||||
_idToFeedDictionary = idDictionary
|
_idToFeedDictionary = idDictionary
|
||||||
feedDictionaryNeedsUpdate = false
|
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() {
|
func updateUnreadCount() {
|
||||||
if fetchingAllUnreadCounts {
|
if fetchingAllUnreadCounts {
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71D22835E9800D9D53D /* FeedSpecifier.swift */; };
|
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71D22835E9800D9D53D /* FeedSpecifier.swift */; };
|
||||||
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71E22835E9800D9D53D /* HTMLFeedFinder.swift */; };
|
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D71E22835E9800D9D53D /* HTMLFeedFinder.swift */; };
|
||||||
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D73022837F3400D9D53D /* InitialFeedDownloader.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 */; };
|
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
|
||||||
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
|
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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
|
||||||
@ -333,6 +335,7 @@
|
|||||||
841974241F6DDCE4006346C4 /* AccountDelegate.swift */,
|
841974241F6DDCE4006346C4 /* AccountDelegate.swift */,
|
||||||
51E3EB40229AF61B00645299 /* AccountError.swift */,
|
51E3EB40229AF61B00645299 /* AccountError.swift */,
|
||||||
846E77531F6F00E300A165E2 /* AccountManager.swift */,
|
846E77531F6F00E300A165E2 /* AccountManager.swift */,
|
||||||
|
5170743B232AEDB500A461A3 /* OPMLFile.swift */,
|
||||||
84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */,
|
84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */,
|
||||||
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
|
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
|
||||||
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
|
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
|
||||||
@ -591,6 +594,7 @@
|
|||||||
5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */,
|
5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */,
|
||||||
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */,
|
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */,
|
||||||
552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */,
|
552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */,
|
||||||
|
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */,
|
||||||
552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */,
|
552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */,
|
||||||
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
|
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
|
||||||
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
|
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
|
||||||
|
@ -77,11 +77,14 @@ final class LocalAccountDelegate: AccountDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use the same mechanism to load local accounts as we do to load the subscription
|
guard let children = loadDocument.children else {
|
||||||
// OPML all accounts.
|
return
|
||||||
BatchUpdate.shared.perform {
|
|
||||||
account.loadOPML(loadDocument)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BatchUpdate.shared.perform {
|
||||||
|
account.loadOPMLItems(children, parentFolder: nil)
|
||||||
|
}
|
||||||
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
112
Frameworks/Account/OPMLFile.swift
Normal file
112
Frameworks/Account/OPMLFile.swift
Normal file
@ -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…
x
Reference in New Issue
Block a user