Move account files to the documents directory and out of the shared container. Issue #1784
This commit is contained in:
parent
31b72221f8
commit
2ae021960b
@ -31,10 +31,9 @@ public extension Notification.Name {
|
|||||||
static let AccountDidDownloadArticles = Notification.Name(rawValue: "AccountDidDownloadArticles")
|
static let AccountDidDownloadArticles = Notification.Name(rawValue: "AccountDidDownloadArticles")
|
||||||
static let AccountStateDidChange = Notification.Name(rawValue: "AccountStateDidChange")
|
static let AccountStateDidChange = Notification.Name(rawValue: "AccountStateDidChange")
|
||||||
static let StatusesDidChange = Notification.Name(rawValue: "StatusesDidChange")
|
static let StatusesDidChange = Notification.Name(rawValue: "StatusesDidChange")
|
||||||
static let WebFeedMetadataDidChange = Notification.Name(rawValue: "WebFeedMetadataDidChange")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AccountType: Int {
|
public enum AccountType: Int, Codable {
|
||||||
// Raw values should not change since they’re stored on disk.
|
// Raw values should not change since they’re stored on disk.
|
||||||
case onMyMac = 1
|
case onMyMac = 1
|
||||||
case feedly = 16
|
case feedly = 16
|
||||||
@ -199,8 +198,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
typealias WebFeedMetadataDictionary = [String: WebFeedMetadata]
|
typealias WebFeedMetadataDictionary = [String: WebFeedMetadata]
|
||||||
var webFeedMetadata = WebFeedMetadataDictionary()
|
var webFeedMetadata = WebFeedMetadataDictionary()
|
||||||
|
|
||||||
var startingUp = true
|
|
||||||
|
|
||||||
public var unreadCount = 0 {
|
public var unreadCount = 0 {
|
||||||
didSet {
|
didSet {
|
||||||
if unreadCount != oldValue {
|
if unreadCount != oldValue {
|
||||||
@ -287,7 +284,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.delegate.accountDidInitialize(self)
|
self.delegate.accountDidInitialize(self)
|
||||||
startingUp = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - API
|
// MARK: - API
|
||||||
@ -416,9 +412,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
public func suspendDatabase() {
|
public func suspendDatabase() {
|
||||||
database.cancelAndSuspend()
|
database.cancelAndSuspend()
|
||||||
save()
|
save()
|
||||||
metadataFile.suspend()
|
|
||||||
webFeedMetadataFile.suspend()
|
|
||||||
opmlFile.suspend()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-open the SQLite database and allow database calls.
|
/// Re-open the SQLite database and allow database calls.
|
||||||
@ -430,12 +423,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
|
|
||||||
/// Reload OPML, etc.
|
/// Reload OPML, etc.
|
||||||
public func resume() {
|
public func resume() {
|
||||||
metadataFile.resume()
|
|
||||||
webFeedMetadataFile.resume()
|
|
||||||
opmlFile.resume()
|
|
||||||
metadataFile.load()
|
|
||||||
webFeedMetadataFile.load()
|
|
||||||
opmlFile.load()
|
|
||||||
fetchAllUnreadCounts()
|
fetchAllUnreadCounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,14 +475,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func resetWebFeedMetadataAndUnreadCounts() {
|
|
||||||
for feed in flattenedWebFeeds() {
|
|
||||||
feed.metadata = webFeedMetadata(feedURL: feed.url, webFeedID: feed.webFeedID)
|
|
||||||
}
|
|
||||||
fetchAllUnreadCounts()
|
|
||||||
NotificationCenter.default.post(name: .WebFeedMetadataDidChange, object: self, userInfo: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -691,9 +670,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
public func structureDidChange() {
|
public func structureDidChange() {
|
||||||
// Feeds were added or deleted. Or folders added or deleted.
|
// Feeds were added or deleted. Or folders added or deleted.
|
||||||
// Or feeds inside folders were added or deleted.
|
// Or feeds inside folders were added or deleted.
|
||||||
if !startingUp {
|
opmlFile.markAsDirty()
|
||||||
opmlFile.markAsDirty()
|
|
||||||
}
|
|
||||||
flattenedWebFeedsNeedUpdate = true
|
flattenedWebFeedsNeedUpdate = true
|
||||||
webFeedDictionaryNeedsUpdate = true
|
webFeedDictionaryNeedsUpdate = true
|
||||||
}
|
}
|
||||||
|
@ -90,14 +90,6 @@ public final class AccountManager: UnreadCountProvider {
|
|||||||
return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray)
|
return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
public convenience init() {
|
|
||||||
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
|
|
||||||
let accountsURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
|
||||||
let accountsFolder = accountsURL!.appendingPathComponent("Accounts").absoluteString
|
|
||||||
let accountsFolderPath = accountsFolder.suffix(from: accountsFolder.index(accountsFolder.startIndex, offsetBy: 7))
|
|
||||||
self.init(accountsFolder: String(accountsFolderPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(accountsFolder: String) {
|
public init(accountsFolder: String) {
|
||||||
self.accountsFolder = accountsFolder
|
self.accountsFolder = accountsFolder
|
||||||
|
|
||||||
|
@ -16,7 +16,13 @@ final class AccountMetadataFile {
|
|||||||
|
|
||||||
private let fileURL: URL
|
private let fileURL: URL
|
||||||
private let account: Account
|
private let account: Account
|
||||||
private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, save: saveCallback)
|
|
||||||
|
private var isDirty = false {
|
||||||
|
didSet {
|
||||||
|
queueSaveToDiskIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5)
|
||||||
|
|
||||||
init(filename: String, account: Account) {
|
init(filename: String, account: Account) {
|
||||||
self.fileURL = URL(fileURLWithPath: filename)
|
self.fileURL = URL(fileURLWithPath: filename)
|
||||||
@ -24,33 +30,12 @@ final class AccountMetadataFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func markAsDirty() {
|
func markAsDirty() {
|
||||||
managedFile.markAsDirty()
|
isDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
managedFile.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func save() {
|
|
||||||
managedFile.saveIfNecessary()
|
|
||||||
}
|
|
||||||
|
|
||||||
func suspend() {
|
|
||||||
managedFile.suspend()
|
|
||||||
}
|
|
||||||
|
|
||||||
func resume() {
|
|
||||||
managedFile.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension AccountMetadataFile {
|
|
||||||
|
|
||||||
func loadCallback() {
|
|
||||||
|
|
||||||
let errorPointer: NSErrorPointer = nil
|
let errorPointer: NSErrorPointer = nil
|
||||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
let fileCoordinator = NSFileCoordinator()
|
||||||
|
|
||||||
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
||||||
if let fileData = try? Data(contentsOf: readURL) {
|
if let fileData = try? Data(contentsOf: readURL) {
|
||||||
@ -63,17 +48,16 @@ private extension AccountMetadataFile {
|
|||||||
if let error = errorPointer?.pointee {
|
if let error = errorPointer?.pointee {
|
||||||
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
|
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveCallback() {
|
func save() {
|
||||||
guard !account.isDeleted else { return }
|
guard !account.isDeleted else { return }
|
||||||
|
|
||||||
let encoder = PropertyListEncoder()
|
let encoder = PropertyListEncoder()
|
||||||
encoder.outputFormat = .binary
|
encoder.outputFormat = .binary
|
||||||
|
|
||||||
let errorPointer: NSErrorPointer = nil
|
let errorPointer: NSErrorPointer = nil
|
||||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
let fileCoordinator = NSFileCoordinator()
|
||||||
|
|
||||||
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
||||||
do {
|
do {
|
||||||
@ -90,3 +74,18 @@ private extension AccountMetadataFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension AccountMetadataFile {
|
||||||
|
|
||||||
|
func queueSaveToDiskIfNeeded() {
|
||||||
|
saveQueue.add(self, #selector(saveToDiskIfNeeded))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func saveToDiskIfNeeded() {
|
||||||
|
if isDirty {
|
||||||
|
isDirty = false
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ public protocol ContainerIdentifiable {
|
|||||||
var containerID: ContainerIdentifier? { get }
|
var containerID: ContainerIdentifier? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ContainerIdentifier: Hashable {
|
public enum ContainerIdentifier: Hashable, Equatable {
|
||||||
case smartFeedController
|
case smartFeedController
|
||||||
case account(String) // accountID
|
case account(String) // accountID
|
||||||
case folder(String, String) // accountID, folderName
|
case folder(String, String) // accountID, folderName
|
||||||
@ -55,3 +55,47 @@ public enum ContainerIdentifier: Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ContainerIdentifier: Encodable {
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case type
|
||||||
|
case accountID
|
||||||
|
case folderName
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
switch self {
|
||||||
|
case .smartFeedController:
|
||||||
|
try container.encode("smartFeedController", forKey: .type)
|
||||||
|
case .account(let accountID):
|
||||||
|
try container.encode("account", forKey: .type)
|
||||||
|
try container.encode(accountID, forKey: .accountID)
|
||||||
|
case .folder(let accountID, let folderName):
|
||||||
|
try container.encode("folder", forKey: .type)
|
||||||
|
try container.encode(accountID, forKey: .accountID)
|
||||||
|
try container.encode(folderName, forKey: .folderName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ContainerIdentifier: Decodable {
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let type = try container.decode(String.self, forKey: .type)
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case "smartFeedController":
|
||||||
|
self = .smartFeedController
|
||||||
|
case "account":
|
||||||
|
let accountID = try container.decode(String.self, forKey: .accountID)
|
||||||
|
self = .account(accountID)
|
||||||
|
default:
|
||||||
|
let accountID = try container.decode(String.self, forKey: .accountID)
|
||||||
|
let folderName = try container.decode(String.self, forKey: .folderName)
|
||||||
|
self = .folder(accountID, folderName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -17,7 +17,13 @@ final class OPMLFile {
|
|||||||
|
|
||||||
private let fileURL: URL
|
private let fileURL: URL
|
||||||
private let account: Account
|
private let account: Account
|
||||||
private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, save: saveCallback)
|
|
||||||
|
private var isDirty = false {
|
||||||
|
didSet {
|
||||||
|
queueSaveToDiskIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5)
|
||||||
|
|
||||||
init(filename: String, account: Account) {
|
init(filename: String, account: Account) {
|
||||||
self.fileURL = URL(fileURLWithPath: filename)
|
self.fileURL = URL(fileURLWithPath: filename)
|
||||||
@ -25,54 +31,26 @@ final class OPMLFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func markAsDirty() {
|
func markAsDirty() {
|
||||||
managedFile.markAsDirty()
|
isDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
managedFile.load()
|
guard let fileData = opmlFileData(), let opmlItems = parsedOPMLItems(fileData: fileData) else {
|
||||||
}
|
|
||||||
|
|
||||||
func save() {
|
|
||||||
managedFile.saveIfNecessary()
|
|
||||||
}
|
|
||||||
|
|
||||||
func suspend() {
|
|
||||||
managedFile.suspend()
|
|
||||||
}
|
|
||||||
|
|
||||||
func resume() {
|
|
||||||
managedFile.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension OPMLFile {
|
|
||||||
|
|
||||||
func loadCallback() {
|
|
||||||
guard let fileData = opmlFileData() else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't rebuild the account if the OPML hasn't changed since the last save
|
|
||||||
guard let opml = String(data: fileData, encoding: .utf8), opml != opmlDocument() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let opmlItems = parsedOPMLItems(fileData: fileData) else { return }
|
|
||||||
|
|
||||||
BatchUpdate.shared.perform {
|
BatchUpdate.shared.perform {
|
||||||
account.topLevelWebFeeds.removeAll()
|
|
||||||
account.loadOPMLItems(opmlItems, parentFolder: nil)
|
account.loadOPMLItems(opmlItems, parentFolder: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveCallback() {
|
func save() {
|
||||||
guard !account.isDeleted else { return }
|
guard !account.isDeleted else { return }
|
||||||
|
|
||||||
let opmlDocumentString = opmlDocument()
|
let opmlDocumentString = opmlDocument()
|
||||||
|
|
||||||
let errorPointer: NSErrorPointer = nil
|
let errorPointer: NSErrorPointer = nil
|
||||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
let fileCoordinator = NSFileCoordinator()
|
||||||
|
|
||||||
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
||||||
do {
|
do {
|
||||||
@ -85,12 +63,28 @@ private extension OPMLFile {
|
|||||||
if let error = errorPointer?.pointee {
|
if let error = errorPointer?.pointee {
|
||||||
os_log(.error, log: log, "OPML save to disk coordination failed: %@.", error.localizedDescription)
|
os_log(.error, log: log, "OPML save to disk coordination failed: %@.", error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension OPMLFile {
|
||||||
|
|
||||||
|
func queueSaveToDiskIfNeeded() {
|
||||||
|
saveQueue.add(self, #selector(saveToDiskIfNeeded))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func saveToDiskIfNeeded() {
|
||||||
|
if isDirty {
|
||||||
|
isDirty = false
|
||||||
|
save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func opmlFileData() -> Data? {
|
func opmlFileData() -> Data? {
|
||||||
var fileData: Data? = nil
|
var fileData: Data? = nil
|
||||||
let errorPointer: NSErrorPointer = nil
|
let errorPointer: NSErrorPointer = nil
|
||||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
let fileCoordinator = NSFileCoordinator()
|
||||||
|
|
||||||
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
||||||
do {
|
do {
|
||||||
|
@ -16,7 +16,13 @@ final class WebFeedMetadataFile {
|
|||||||
|
|
||||||
private let fileURL: URL
|
private let fileURL: URL
|
||||||
private let account: Account
|
private let account: Account
|
||||||
private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, save: saveCallback)
|
|
||||||
|
private var isDirty = false {
|
||||||
|
didSet {
|
||||||
|
queueSaveToDiskIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5)
|
||||||
|
|
||||||
init(filename: String, account: Account) {
|
init(filename: String, account: Account) {
|
||||||
self.fileURL = URL(fileURLWithPath: filename)
|
self.fileURL = URL(fileURLWithPath: filename)
|
||||||
@ -24,33 +30,12 @@ final class WebFeedMetadataFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func markAsDirty() {
|
func markAsDirty() {
|
||||||
managedFile.markAsDirty()
|
isDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
managedFile.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func save() {
|
|
||||||
managedFile.saveIfNecessary()
|
|
||||||
}
|
|
||||||
|
|
||||||
func suspend() {
|
|
||||||
managedFile.suspend()
|
|
||||||
}
|
|
||||||
|
|
||||||
func resume() {
|
|
||||||
managedFile.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension WebFeedMetadataFile {
|
|
||||||
|
|
||||||
func loadCallback() {
|
|
||||||
|
|
||||||
let errorPointer: NSErrorPointer = nil
|
let errorPointer: NSErrorPointer = nil
|
||||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
let fileCoordinator = NSFileCoordinator()
|
||||||
|
|
||||||
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
||||||
if let fileData = try? Data(contentsOf: readURL) {
|
if let fileData = try? Data(contentsOf: readURL) {
|
||||||
@ -58,19 +43,14 @@ private extension WebFeedMetadataFile {
|
|||||||
account.webFeedMetadata = (try? decoder.decode(Account.WebFeedMetadataDictionary.self, from: fileData)) ?? Account.WebFeedMetadataDictionary()
|
account.webFeedMetadata = (try? decoder.decode(Account.WebFeedMetadataDictionary.self, from: fileData)) ?? Account.WebFeedMetadataDictionary()
|
||||||
}
|
}
|
||||||
account.webFeedMetadata.values.forEach { $0.delegate = account }
|
account.webFeedMetadata.values.forEach { $0.delegate = account }
|
||||||
if !account.startingUp {
|
|
||||||
account.resetWebFeedMetadataAndUnreadCounts()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if let error = errorPointer?.pointee {
|
if let error = errorPointer?.pointee {
|
||||||
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
|
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveCallback() {
|
func save() {
|
||||||
guard !account.isDeleted else { return }
|
guard !account.isDeleted else { return }
|
||||||
|
|
||||||
let feedMetadata = metadataForOnlySubscribedToFeeds()
|
let feedMetadata = metadataForOnlySubscribedToFeeds()
|
||||||
@ -79,7 +59,7 @@ private extension WebFeedMetadataFile {
|
|||||||
encoder.outputFormat = .binary
|
encoder.outputFormat = .binary
|
||||||
|
|
||||||
let errorPointer: NSErrorPointer = nil
|
let errorPointer: NSErrorPointer = nil
|
||||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
let fileCoordinator = NSFileCoordinator()
|
||||||
|
|
||||||
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
||||||
do {
|
do {
|
||||||
@ -95,6 +75,21 @@ private extension WebFeedMetadataFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension WebFeedMetadataFile {
|
||||||
|
|
||||||
|
func queueSaveToDiskIfNeeded() {
|
||||||
|
saveQueue.add(self, #selector(saveToDiskIfNeeded))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func saveToDiskIfNeeded() {
|
||||||
|
if isDirty {
|
||||||
|
isDirty = false
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func metadataForOnlySubscribedToFeeds() -> Account.WebFeedMetadataDictionary {
|
private func metadataForOnlySubscribedToFeeds() -> Account.WebFeedMetadataDictionary {
|
||||||
let webFeedIDs = account.idToWebFeedDictionary.keys
|
let webFeedIDs = account.idToWebFeedDictionary.keys
|
||||||
return account.webFeedMetadata.filter { (feedID: String, metadata: WebFeedMetadata) -> Bool in
|
return account.webFeedMetadata.filter { (feedID: String, metadata: WebFeedMetadata) -> Bool in
|
||||||
|
@ -207,7 +207,14 @@ private extension DetailWebViewController {
|
|||||||
|
|
||||||
func reloadArticleImage() {
|
func reloadArticleImage() {
|
||||||
guard let article = article else { return }
|
guard let article = article else { return }
|
||||||
webView?.evaluateJavaScript("reloadArticleImage(\"\(article.articleID)\")")
|
|
||||||
|
var components = URLComponents()
|
||||||
|
components.scheme = ArticleRenderer.imageIconScheme
|
||||||
|
components.path = article.articleID
|
||||||
|
|
||||||
|
if let imageSrc = components.string {
|
||||||
|
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadHTML() {
|
func reloadHTML() {
|
||||||
|
@ -137,7 +137,6 @@
|
|||||||
51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; };
|
51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; };
|
||||||
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; };
|
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; };
|
||||||
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; };
|
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; };
|
||||||
51A9A5E02380C3F10033AADF /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; };
|
|
||||||
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; };
|
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; };
|
||||||
51A9A5E42380C8880033AADF /* ShareFolderPickerAccountCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */; };
|
51A9A5E42380C8880033AADF /* ShareFolderPickerAccountCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */; };
|
||||||
51A9A5E62380C8B20033AADF /* ShareFolderPickerFolderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */; };
|
51A9A5E62380C8B20033AADF /* ShareFolderPickerFolderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */; };
|
||||||
@ -150,6 +149,19 @@
|
|||||||
51A9A5F52380F6A60033AADF /* ModalNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */; };
|
51A9A5F52380F6A60033AADF /* ModalNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */; };
|
||||||
51A9A60A2382FD240033AADF /* PoppableGestureRecognizerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */; };
|
51A9A60A2382FD240033AADF /* PoppableGestureRecognizerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */; };
|
||||||
51AB8AB323B7F4C6008F147D /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AB8AB223B7F4C6008F147D /* WebViewController.swift */; };
|
51AB8AB323B7F4C6008F147D /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AB8AB223B7F4C6008F147D /* WebViewController.swift */; };
|
||||||
|
51B5C87723F22B8200032075 /* ExtensionContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87623F22B8200032075 /* ExtensionContainers.swift */; };
|
||||||
|
51B5C87B23F2317700032075 /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; };
|
||||||
|
51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */; };
|
||||||
|
51B5C8B923F368D000032075 /* ExtensionContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87623F22B8200032075 /* ExtensionContainers.swift */; };
|
||||||
|
51B5C8BA23F368D000032075 /* ExtensionContainersFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */; };
|
||||||
|
51B5C8BB23F368D000032075 /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; };
|
||||||
|
51B5C8BE23F37B2400032075 /* ShareDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BC23F3780900032075 /* ShareDefaultContainer.swift */; };
|
||||||
|
51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */; };
|
||||||
|
51B5C8C123F3A0DB00032075 /* ExtensionFeedAddRequestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */; };
|
||||||
|
51B5C8E423F4BBFA00032075 /* ExtensionContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87623F22B8200032075 /* ExtensionContainers.swift */; };
|
||||||
|
51B5C8E523F4BBFA00032075 /* ExtensionContainersFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */; };
|
||||||
|
51B5C8E623F4BBFA00032075 /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; };
|
||||||
|
51B5C8E723F4BBFA00032075 /* ExtensionFeedAddRequestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */; };
|
||||||
51B62E68233186730085F949 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B62E67233186730085F949 /* IconView.swift */; };
|
51B62E68233186730085F949 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B62E67233186730085F949 /* IconView.swift */; };
|
||||||
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
|
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
|
||||||
51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
|
51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
|
||||||
@ -225,6 +237,7 @@
|
|||||||
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; };
|
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; };
|
||||||
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
|
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
|
||||||
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */; };
|
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */; };
|
||||||
|
51C9DE8623F09FAA003D5A6D /* AccountMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */; };
|
||||||
51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; };
|
51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; };
|
||||||
51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; };
|
51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; };
|
||||||
51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; };
|
51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; };
|
||||||
@ -1331,6 +1344,11 @@
|
|||||||
51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalNavigationController.swift; sourceTree = "<group>"; };
|
51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalNavigationController.swift; sourceTree = "<group>"; };
|
||||||
51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoppableGestureRecognizerDelegate.swift; sourceTree = "<group>"; };
|
51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoppableGestureRecognizerDelegate.swift; sourceTree = "<group>"; };
|
||||||
51AB8AB223B7F4C6008F147D /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
51AB8AB223B7F4C6008F147D /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
||||||
|
51B5C87623F22B8200032075 /* ExtensionContainers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionContainers.swift; sourceTree = "<group>"; };
|
||||||
|
51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionFeedAddRequest.swift; sourceTree = "<group>"; };
|
||||||
|
51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionContainersFile.swift; sourceTree = "<group>"; };
|
||||||
|
51B5C8BC23F3780900032075 /* ShareDefaultContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareDefaultContainer.swift; sourceTree = "<group>"; };
|
||||||
|
51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionFeedAddRequestFile.swift; sourceTree = "<group>"; };
|
||||||
51B62E67233186730085F949 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
|
51B62E67233186730085F949 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
|
||||||
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = "<group>"; };
|
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
|
51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
|
||||||
@ -1356,6 +1374,7 @@
|
|||||||
51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
|
51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
|
51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
|
||||||
51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = "<group>"; };
|
51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = "<group>"; };
|
||||||
|
51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMigrator.swift; sourceTree = "<group>"; };
|
||||||
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = "<group>"; };
|
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = "<group>"; };
|
||||||
51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = "<group>"; };
|
51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = "<group>"; };
|
||||||
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
|
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
|
||||||
@ -1793,6 +1812,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
513C5CE8232571C2003D4054 /* ShareViewController.swift */,
|
513C5CE8232571C2003D4054 /* ShareViewController.swift */,
|
||||||
|
51B5C8BC23F3780900032075 /* ShareDefaultContainer.swift */,
|
||||||
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */,
|
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */,
|
||||||
51A9A5E72380CA130033AADF /* ShareFolderPickerCell.swift */,
|
51A9A5E72380CA130033AADF /* ShareFolderPickerCell.swift */,
|
||||||
51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */,
|
51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */,
|
||||||
@ -1881,6 +1901,17 @@
|
|||||||
path = Activity;
|
path = Activity;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
51B5C85A23F22A7A00032075 /* CommonExtension */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
51B5C87623F22B8200032075 /* ExtensionContainers.swift */,
|
||||||
|
51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */,
|
||||||
|
51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */,
|
||||||
|
51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */,
|
||||||
|
);
|
||||||
|
path = CommonExtension;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
51C45245226506C800C03939 /* UIKit Extensions */ = {
|
51C45245226506C800C03939 /* UIKit Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -2612,6 +2643,7 @@
|
|||||||
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */,
|
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */,
|
||||||
C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */,
|
C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */,
|
||||||
51B62E67233186730085F949 /* IconView.swift */,
|
51B62E67233186730085F949 /* IconView.swift */,
|
||||||
|
51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */,
|
||||||
51C4525D226508F600C03939 /* MasterFeed */,
|
51C4525D226508F600C03939 /* MasterFeed */,
|
||||||
51C4526D2265091600C03939 /* MasterTimeline */,
|
51C4526D2265091600C03939 /* MasterTimeline */,
|
||||||
51C4527D2265092C00C03939 /* Article */,
|
51C4527D2265092C00C03939 /* Article */,
|
||||||
@ -2621,6 +2653,7 @@
|
|||||||
513145F9235A55A700387FDC /* Intents */,
|
513145F9235A55A700387FDC /* Intents */,
|
||||||
5183CCEB227117C70010922C /* Settings */,
|
5183CCEB227117C70010922C /* Settings */,
|
||||||
51C45245226506C800C03939 /* UIKit Extensions */,
|
51C45245226506C800C03939 /* UIKit Extensions */,
|
||||||
|
51B5C85A23F22A7A00032075 /* CommonExtension */,
|
||||||
513C5CE7232571C2003D4054 /* ShareExtension */,
|
513C5CE7232571C2003D4054 /* ShareExtension */,
|
||||||
51314643235A7C2300387FDC /* IntentsExtension */,
|
51314643235A7C2300387FDC /* IntentsExtension */,
|
||||||
84C9FC9A2262A1A900D921D6 /* Resources */,
|
84C9FC9A2262A1A900D921D6 /* Resources */,
|
||||||
@ -3644,8 +3677,12 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
513146B3235A81A400387FDC /* AddWebFeedIntentHandler.swift in Sources */,
|
513146B3235A81A400387FDC /* AddWebFeedIntentHandler.swift in Sources */,
|
||||||
|
51B5C8E623F4BBFA00032075 /* ExtensionFeedAddRequest.swift in Sources */,
|
||||||
51314705235C41FC00387FDC /* Intents.intentdefinition in Sources */,
|
51314705235C41FC00387FDC /* Intents.intentdefinition in Sources */,
|
||||||
|
51B5C8E523F4BBFA00032075 /* ExtensionContainersFile.swift in Sources */,
|
||||||
51314668235A7E4600387FDC /* IntentHandler.swift in Sources */,
|
51314668235A7E4600387FDC /* IntentHandler.swift in Sources */,
|
||||||
|
51B5C8E423F4BBFA00032075 /* ExtensionContainers.swift in Sources */,
|
||||||
|
51B5C8E723F4BBFA00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -3654,13 +3691,17 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */,
|
515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */,
|
||||||
|
51B5C8B923F368D000032075 /* ExtensionContainers.swift in Sources */,
|
||||||
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */,
|
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */,
|
||||||
513C5CE9232571C2003D4054 /* ShareViewController.swift in Sources */,
|
513C5CE9232571C2003D4054 /* ShareViewController.swift in Sources */,
|
||||||
51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */,
|
51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */,
|
||||||
51A9A5E02380C3F10033AADF /* AddWebFeedDefaultContainer.swift in Sources */,
|
51B5C8BE23F37B2400032075 /* ShareDefaultContainer.swift in Sources */,
|
||||||
|
51B5C8BA23F368D000032075 /* ExtensionContainersFile.swift in Sources */,
|
||||||
|
51B5C8BB23F368D000032075 /* ExtensionFeedAddRequest.swift in Sources */,
|
||||||
51A9A5E82380CA130033AADF /* ShareFolderPickerCell.swift in Sources */,
|
51A9A5E82380CA130033AADF /* ShareFolderPickerCell.swift in Sources */,
|
||||||
51A9A5EF2380D63B0033AADF /* IconImage.swift in Sources */,
|
51A9A5EF2380D63B0033AADF /* IconImage.swift in Sources */,
|
||||||
51A9A5ED2380D6000033AADF /* AppAssets.swift in Sources */,
|
51A9A5ED2380D6000033AADF /* AppAssets.swift in Sources */,
|
||||||
|
51B5C8C123F3A0DB00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
|
||||||
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */,
|
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -3899,9 +3940,11 @@
|
|||||||
51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */,
|
51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */,
|
||||||
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
|
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
|
||||||
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */,
|
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */,
|
||||||
|
51B5C87723F22B8200032075 /* ExtensionContainers.swift in Sources */,
|
||||||
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */,
|
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */,
|
||||||
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
|
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
|
||||||
514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */,
|
514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */,
|
||||||
|
51B5C87B23F2317700032075 /* ExtensionFeedAddRequest.swift in Sources */,
|
||||||
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */,
|
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */,
|
||||||
512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */,
|
512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */,
|
||||||
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
|
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
|
||||||
@ -3925,6 +3968,7 @@
|
|||||||
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
|
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
|
||||||
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */,
|
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */,
|
||||||
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */,
|
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */,
|
||||||
|
51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */,
|
||||||
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
|
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
|
||||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
|
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
|
||||||
C5A6ED6D23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift in Sources */,
|
C5A6ED6D23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift in Sources */,
|
||||||
@ -3949,8 +3993,10 @@
|
|||||||
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */,
|
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */,
|
||||||
5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */,
|
5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */,
|
||||||
51C452882265093600C03939 /* AddWebFeedViewController.swift in Sources */,
|
51C452882265093600C03939 /* AddWebFeedViewController.swift in Sources */,
|
||||||
|
51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
|
||||||
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */,
|
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */,
|
||||||
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
|
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
|
||||||
|
51C9DE8623F09FAA003D5A6D /* AccountMigrator.swift in Sources */,
|
||||||
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */,
|
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */,
|
||||||
518651DA235621840078E021 /* ImageTransition.swift in Sources */,
|
518651DA235621840078E021 /* ImageTransition.swift in Sources */,
|
||||||
51C266EA238C334800F53014 /* ContextMenuPreviewViewController.swift in Sources */,
|
51C266EA238C334800F53014 /* ContextMenuPreviewViewController.swift in Sources */,
|
||||||
|
@ -79,9 +79,9 @@ function flattenPreElements() {
|
|||||||
ElementUnwrapper.unwrapAppropriateChildren("div.articleBody td > pre");
|
ElementUnwrapper.unwrapAppropriateChildren("div.articleBody td > pre");
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadArticleImage(articleID) {
|
function reloadArticleImage(imageSrc) {
|
||||||
var image = document.getElementById("nnwImageIcon");
|
var image = document.getElementById("nnwImageIcon");
|
||||||
image.src = "nnwImageIcon://" + articleID;
|
image.src = imageSrc;
|
||||||
}
|
}
|
||||||
|
|
||||||
function error() {
|
function error() {
|
||||||
|
24
iOS/AccountMigrator.swift
Normal file
24
iOS/AccountMigrator.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// AccountMigrator.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 2/9/20.
|
||||||
|
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct AccountMigrator {
|
||||||
|
|
||||||
|
static func migrate() {
|
||||||
|
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
|
||||||
|
let containerAccountsURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
||||||
|
let containerAccountsFolder = containerAccountsURL!.appendingPathComponent("Accounts")
|
||||||
|
|
||||||
|
let documentAccountURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
|
let documentAccountsFolder = documentAccountURL.appendingPathComponent("Accounts")
|
||||||
|
|
||||||
|
try? FileManager.default.moveItem(at: containerAccountsFolder, to: documentAccountsFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -41,6 +41,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
var imageDownloader: ImageDownloader!
|
var imageDownloader: ImageDownloader!
|
||||||
var authorAvatarDownloader: AuthorAvatarDownloader!
|
var authorAvatarDownloader: AuthorAvatarDownloader!
|
||||||
var webFeedIconDownloader: WebFeedIconDownloader!
|
var webFeedIconDownloader: WebFeedIconDownloader!
|
||||||
|
var extensionContainersFile: ExtensionContainersFile!
|
||||||
|
var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile!
|
||||||
|
|
||||||
var unreadCount = 0 {
|
var unreadCount = 0 {
|
||||||
didSet {
|
didSet {
|
||||||
@ -58,7 +60,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
super.init()
|
super.init()
|
||||||
appDelegate = self
|
appDelegate = self
|
||||||
|
|
||||||
AccountManager.shared = AccountManager()
|
AccountMigrator.migrate()
|
||||||
|
|
||||||
|
let documentAccountURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
|
let documentAccountsFolder = documentAccountURL.appendingPathComponent("Accounts").absoluteString
|
||||||
|
let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7)))
|
||||||
|
AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
|
||||||
@ -97,6 +104,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
UNUserNotificationCenter.current().delegate = self
|
UNUserNotificationCenter.current().delegate = self
|
||||||
userNotificationManager = UserNotificationManager()
|
userNotificationManager = UserNotificationManager()
|
||||||
|
|
||||||
|
extensionContainersFile = ExtensionContainersFile()
|
||||||
|
extensionFeedAddRequestFile = ExtensionFeedAddRequestFile()
|
||||||
|
|
||||||
syncTimer = ArticleStatusSyncTimer()
|
syncTimer = ArticleStatusSyncTimer()
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@ -132,6 +142,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prepareAccountsForBackground() {
|
func prepareAccountsForBackground() {
|
||||||
|
extensionFeedAddRequestFile.suspend()
|
||||||
syncTimer?.invalidate()
|
syncTimer?.invalidate()
|
||||||
scheduleBackgroundFeedRefresh()
|
scheduleBackgroundFeedRefresh()
|
||||||
syncArticleStatus()
|
syncArticleStatus()
|
||||||
@ -139,6 +150,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prepareAccountsForForeground() {
|
func prepareAccountsForForeground() {
|
||||||
|
extensionFeedAddRequestFile.resume()
|
||||||
|
|
||||||
if let lastRefresh = AppDefaults.lastRefresh {
|
if let lastRefresh = AppDefaults.lastRefresh {
|
||||||
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
|
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
|
||||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
||||||
|
@ -519,7 +519,14 @@ private extension WebViewController {
|
|||||||
|
|
||||||
func reloadArticleImage() {
|
func reloadArticleImage() {
|
||||||
guard let article = article else { return }
|
guard let article = article else { return }
|
||||||
webView?.evaluateJavaScript("reloadArticleImage(\"\(article.articleID)\")")
|
|
||||||
|
var components = URLComponents()
|
||||||
|
components.scheme = ArticleRenderer.imageIconScheme
|
||||||
|
components.path = article.articleID
|
||||||
|
|
||||||
|
if let imageSrc = components.string {
|
||||||
|
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageWasClicked(body: String?) {
|
func imageWasClicked(body: String?) {
|
||||||
|
94
iOS/CommonExtension/ExtensionContainers.swift
Normal file
94
iOS/CommonExtension/ExtensionContainers.swift
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
//
|
||||||
|
// ExtensionContainers.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 2/10/20.
|
||||||
|
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Account
|
||||||
|
|
||||||
|
protocol ExtensionContainer: ContainerIdentifiable, Codable {
|
||||||
|
var name: String { get }
|
||||||
|
var accountID: String { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExtensionContainers: Codable {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
let accounts: [ExtensionAccount]
|
||||||
|
|
||||||
|
var flattened: [ExtensionContainer] {
|
||||||
|
return accounts.reduce([ExtensionContainer](), { (containers, account) in
|
||||||
|
var result = containers
|
||||||
|
result.append(account)
|
||||||
|
result.append(contentsOf: account.folders)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func findAccount(forName name: String) -> ExtensionAccount? {
|
||||||
|
return accounts.first(where: { $0.name == name })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExtensionAccount: ExtensionContainer {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name
|
||||||
|
case accountID
|
||||||
|
case type
|
||||||
|
case disallowFeedInRootFolder
|
||||||
|
case containerID
|
||||||
|
case folders
|
||||||
|
}
|
||||||
|
|
||||||
|
let name: String
|
||||||
|
let accountID: String
|
||||||
|
let type: AccountType
|
||||||
|
let disallowFeedInRootFolder: Bool
|
||||||
|
let containerID: ContainerIdentifier?
|
||||||
|
let folders: [ExtensionFolder]
|
||||||
|
|
||||||
|
init(account: Account) {
|
||||||
|
self.name = account.nameForDisplay
|
||||||
|
self.accountID = account.accountID
|
||||||
|
self.type = account.type
|
||||||
|
self.disallowFeedInRootFolder = account.behaviors.contains(.disallowFeedInRootFolder)
|
||||||
|
self.containerID = account.containerID
|
||||||
|
self.folders = account.sortedFolders?.map { ExtensionFolder(folder: $0) } ?? [ExtensionFolder]()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findFolder(forName name: String) -> ExtensionFolder? {
|
||||||
|
return folders.first(where: { $0.name == name })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExtensionFolder: ExtensionContainer {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case accountName
|
||||||
|
case accountID
|
||||||
|
case name
|
||||||
|
case containerID
|
||||||
|
}
|
||||||
|
|
||||||
|
let accountName: String
|
||||||
|
let accountID: String
|
||||||
|
let name: String
|
||||||
|
let containerID: ContainerIdentifier?
|
||||||
|
|
||||||
|
init(folder: Folder) {
|
||||||
|
self.accountName = folder.account?.nameForDisplay ?? ""
|
||||||
|
self.accountID = folder.account?.accountID ?? ""
|
||||||
|
self.name = folder.nameForDisplay
|
||||||
|
self.containerID = folder.containerID
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
107
iOS/CommonExtension/ExtensionContainersFile.swift
Normal file
107
iOS/CommonExtension/ExtensionContainersFile.swift
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// ExtensionContainersFile.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 2/10/20.
|
||||||
|
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import os.log
|
||||||
|
import RSCore
|
||||||
|
import RSParser
|
||||||
|
import Account
|
||||||
|
|
||||||
|
final class ExtensionContainersFile {
|
||||||
|
|
||||||
|
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile")
|
||||||
|
|
||||||
|
private static var filePath: String = {
|
||||||
|
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
|
||||||
|
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
||||||
|
return containerURL!.appendingPathComponent("extension_containers.plist").path
|
||||||
|
}()
|
||||||
|
|
||||||
|
private var isDirty = false {
|
||||||
|
didSet {
|
||||||
|
queueSaveToDiskIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5)
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if !FileManager.default.fileExists(atPath: ExtensionContainersFile.filePath) {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .UserDidAddAccount, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .UserDidDeleteAccount, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .AccountStateDidChange, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .ChildrenDidChange, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads and decodes the shared plist file.
|
||||||
|
static func read() -> ExtensionContainers? {
|
||||||
|
let errorPointer: NSErrorPointer = nil
|
||||||
|
let fileCoordinator = NSFileCoordinator()
|
||||||
|
let fileURL = URL(fileURLWithPath: ExtensionContainersFile.filePath)
|
||||||
|
var extensionContainers: ExtensionContainers? = nil
|
||||||
|
|
||||||
|
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
||||||
|
if let fileData = try? Data(contentsOf: readURL) {
|
||||||
|
let decoder = PropertyListDecoder()
|
||||||
|
extensionContainers = try? decoder.decode(ExtensionContainers.self, from: fileData)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if let error = errorPointer?.pointee {
|
||||||
|
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensionContainers
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ExtensionContainersFile {
|
||||||
|
|
||||||
|
@objc func markAsDirty() {
|
||||||
|
isDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func queueSaveToDiskIfNeeded() {
|
||||||
|
saveQueue.add(self, #selector(saveToDiskIfNeeded))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func saveToDiskIfNeeded() {
|
||||||
|
if isDirty {
|
||||||
|
isDirty = false
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func save() {
|
||||||
|
let encoder = PropertyListEncoder()
|
||||||
|
encoder.outputFormat = .binary
|
||||||
|
|
||||||
|
let errorPointer: NSErrorPointer = nil
|
||||||
|
let fileCoordinator = NSFileCoordinator()
|
||||||
|
let fileURL = URL(fileURLWithPath: ExtensionContainersFile.filePath)
|
||||||
|
|
||||||
|
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
||||||
|
do {
|
||||||
|
let extensionAccounts = AccountManager.shared.sortedActiveAccounts.map { ExtensionAccount(account: $0) }
|
||||||
|
let extensionContainers = ExtensionContainers(accounts: extensionAccounts)
|
||||||
|
let data = try encoder.encode(extensionContainers)
|
||||||
|
try data.write(to: writeURL)
|
||||||
|
} catch let error as NSError {
|
||||||
|
os_log(.error, log: Self.log, "Save to disk failed: %@.", error.localizedDescription)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if let error = errorPointer?.pointee {
|
||||||
|
os_log(.error, log: Self.log, "Save to disk coordination failed: %@.", error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
24
iOS/CommonExtension/ExtensionFeedAddRequest.swift
Normal file
24
iOS/CommonExtension/ExtensionFeedAddRequest.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// ExtensionFeedAddRequest.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 2/10/20.
|
||||||
|
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Account
|
||||||
|
|
||||||
|
struct ExtensionFeedAddRequest: Codable {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name
|
||||||
|
case feedURL
|
||||||
|
case destinationContainerID
|
||||||
|
}
|
||||||
|
|
||||||
|
let name: String?
|
||||||
|
let feedURL: URL
|
||||||
|
let destinationContainerID: ContainerIdentifier
|
||||||
|
|
||||||
|
}
|
160
iOS/CommonExtension/ExtensionFeedAddRequestFile.swift
Normal file
160
iOS/CommonExtension/ExtensionFeedAddRequestFile.swift
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
//
|
||||||
|
// ExtensionFeedAddRequestFile.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 2/11/20.
|
||||||
|
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import os.log
|
||||||
|
import Account
|
||||||
|
|
||||||
|
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
|
||||||
|
|
||||||
|
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
|
||||||
|
|
||||||
|
private static var filePath: String = {
|
||||||
|
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
|
||||||
|
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
||||||
|
return containerURL!.appendingPathComponent("extension_feed_add_request.plist").path
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let operationQueue: OperationQueue
|
||||||
|
|
||||||
|
var presentedItemURL: URL? {
|
||||||
|
return URL(fileURLWithPath: ExtensionFeedAddRequestFile.filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var presentedItemOperationQueue: OperationQueue {
|
||||||
|
return operationQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
operationQueue = OperationQueue()
|
||||||
|
operationQueue.maxConcurrentOperationCount = 1
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
NSFileCoordinator.addFilePresenter(self)
|
||||||
|
process()
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentedItemDidChange() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.process()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resume() {
|
||||||
|
NSFileCoordinator.addFilePresenter(self)
|
||||||
|
process()
|
||||||
|
}
|
||||||
|
|
||||||
|
func suspend() {
|
||||||
|
NSFileCoordinator.removeFilePresenter(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func save(_ feedAddRequest: ExtensionFeedAddRequest) {
|
||||||
|
|
||||||
|
let decoder = PropertyListDecoder()
|
||||||
|
let encoder = PropertyListEncoder()
|
||||||
|
encoder.outputFormat = .binary
|
||||||
|
|
||||||
|
let errorPointer: NSErrorPointer = nil
|
||||||
|
let fileCoordinator = NSFileCoordinator()
|
||||||
|
let fileURL = URL(fileURLWithPath: ExtensionFeedAddRequestFile.filePath)
|
||||||
|
|
||||||
|
fileCoordinator.coordinate(writingItemAt: fileURL, options: [.forMerging], error: errorPointer, byAccessor: { url in
|
||||||
|
do {
|
||||||
|
|
||||||
|
var requests: [ExtensionFeedAddRequest]
|
||||||
|
if let fileData = try? Data(contentsOf: url),
|
||||||
|
let decodedRequests = try? decoder.decode([ExtensionFeedAddRequest].self, from: fileData) {
|
||||||
|
requests = decodedRequests
|
||||||
|
} else {
|
||||||
|
requests = [ExtensionFeedAddRequest]()
|
||||||
|
}
|
||||||
|
|
||||||
|
requests.append(feedAddRequest)
|
||||||
|
|
||||||
|
let data = try encoder.encode(requests)
|
||||||
|
try data.write(to: url)
|
||||||
|
|
||||||
|
} catch let error as NSError {
|
||||||
|
os_log(.error, log: Self.log, "Save to disk failed: %@.", error.localizedDescription)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if let error = errorPointer?.pointee {
|
||||||
|
os_log(.error, log: Self.log, "Save to disk coordination failed: %@.", error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ExtensionFeedAddRequestFile {
|
||||||
|
|
||||||
|
func process() {
|
||||||
|
|
||||||
|
let decoder = PropertyListDecoder()
|
||||||
|
let encoder = PropertyListEncoder()
|
||||||
|
encoder.outputFormat = .binary
|
||||||
|
|
||||||
|
let errorPointer: NSErrorPointer = nil
|
||||||
|
let fileCoordinator = NSFileCoordinator(filePresenter: self)
|
||||||
|
let fileURL = URL(fileURLWithPath: ExtensionFeedAddRequestFile.filePath)
|
||||||
|
|
||||||
|
var requests: [ExtensionFeedAddRequest]? = nil
|
||||||
|
|
||||||
|
fileCoordinator.coordinate(writingItemAt: fileURL, options: [.forMerging], error: errorPointer, byAccessor: { url in
|
||||||
|
do {
|
||||||
|
|
||||||
|
if let fileData = try? Data(contentsOf: url),
|
||||||
|
let decodedRequests = try? decoder.decode([ExtensionFeedAddRequest].self, from: fileData) {
|
||||||
|
requests = decodedRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = try encoder.encode([ExtensionFeedAddRequest]())
|
||||||
|
try data.write(to: url)
|
||||||
|
|
||||||
|
} catch let error as NSError {
|
||||||
|
os_log(.error, log: Self.log, "Save to disk failed: %@.", error.localizedDescription)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if let error = errorPointer?.pointee {
|
||||||
|
os_log(.error, log: Self.log, "Save to disk coordination failed: %@.", error.localizedDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
requests?.forEach { processRequest($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func processRequest(_ request: ExtensionFeedAddRequest) {
|
||||||
|
var destinationAccountID: String? = nil
|
||||||
|
switch request.destinationContainerID {
|
||||||
|
case .account(let accountID):
|
||||||
|
destinationAccountID = accountID
|
||||||
|
case .folder(let accountID, _):
|
||||||
|
destinationAccountID = accountID
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let accountID = destinationAccountID, let account = AccountManager.shared.existingAccount(with: accountID) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var destinationContainer: Container? = nil
|
||||||
|
if account.containerID == request.destinationContainerID {
|
||||||
|
destinationContainer = account
|
||||||
|
} else {
|
||||||
|
destinationContainer = account.folders?.first(where: { $0.containerID == request.destinationContainerID })
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let container = destinationContainer else { return }
|
||||||
|
|
||||||
|
account.createWebFeed(url: request.feedURL.absoluteString, name: request.name, container: container) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,15 +7,24 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Intents
|
import Intents
|
||||||
import Account
|
|
||||||
|
public enum AddWebFeedIntentHandlerError: LocalizedError {
|
||||||
|
|
||||||
|
case communicationFailure
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .communicationFailure:
|
||||||
|
return NSLocalizedString("Unable to communicate with NetNewsWire.", comment: "Communication failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
DispatchQueue.main.sync {
|
|
||||||
AccountManager.shared = AccountManager()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func resolveUrl(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedUrlResolutionResult) -> Void) {
|
public func resolveUrl(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedUrlResolutionResult) -> Void) {
|
||||||
@ -27,10 +36,13 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func provideAccountNameOptions(for intent: AddWebFeedIntent, with completion: @escaping ([String]?, Error?) -> Void) {
|
public func provideAccountNameOptions(for intent: AddWebFeedIntent, with completion: @escaping ([String]?, Error?) -> Void) {
|
||||||
DispatchQueue.main.async {
|
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||||
let accountNames = AccountManager.shared.activeAccounts.compactMap { $0.nameForDisplay }
|
completion(nil, AddWebFeedIntentHandlerError.communicationFailure)
|
||||||
completion(accountNames, nil)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let accountNames = extensionContainers.accounts.map { $0.name }
|
||||||
|
completion(accountNames, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func resolveAccountName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedAccountNameResolutionResult) -> Void) {
|
public func resolveAccountName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedAccountNameResolutionResult) -> Void) {
|
||||||
@ -38,25 +50,32 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
|||||||
completion(AddWebFeedAccountNameResolutionResult.notRequired())
|
completion(AddWebFeedAccountNameResolutionResult.notRequired())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
|
||||||
if AccountManager.shared.findActiveAccount(forDisplayName: accountName) == nil {
|
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||||
completion(.unsupported(forReason: .invalid))
|
completion(.unsupported(forReason: .communication))
|
||||||
} else {
|
return
|
||||||
completion(.success(with: accountName))
|
}
|
||||||
}
|
|
||||||
|
if extensionContainers.findAccount(forName: accountName) == nil {
|
||||||
|
completion(.unsupported(forReason: .invalid))
|
||||||
|
} else {
|
||||||
|
completion(.success(with: accountName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func provideFolderNameOptions(for intent: AddWebFeedIntent, with completion: @escaping ([String]?, Error?) -> Void) {
|
public func provideFolderNameOptions(for intent: AddWebFeedIntent, with completion: @escaping ([String]?, Error?) -> Void) {
|
||||||
DispatchQueue.main.async {
|
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||||
guard let accountName = intent.accountName, let account = AccountManager.shared.findActiveAccount(forDisplayName: accountName) else {
|
completion(nil, AddWebFeedIntentHandlerError.communicationFailure)
|
||||||
completion([String](), nil)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let folderNames = account.folders?.map { $0.nameForDisplay }
|
|
||||||
completion(folderNames, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let accountName = intent.accountName, let account = extensionContainers.findAccount(forName: accountName) else {
|
||||||
|
completion([String](), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let folderNames = account.folders.map { $0.name }
|
||||||
|
completion(folderNames, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func resolveFolderName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedFolderNameResolutionResult) -> Void) {
|
public func resolveFolderName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedFolderNameResolutionResult) -> Void) {
|
||||||
@ -65,73 +84,60 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||||
guard let account = AccountManager.shared.findActiveAccount(forDisplayName: accountName) else {
|
completion(.unsupported(forReason: .communication))
|
||||||
completion(.unsupported(forReason: .invalid))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if account.findFolder(withDisplayName: folderName) == nil {
|
|
||||||
completion(.unsupported(forReason: .invalid))
|
|
||||||
} else {
|
|
||||||
completion(.success(with: folderName))
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let account = extensionContainers.findAccount(forName: accountName) else {
|
||||||
|
completion(.unsupported(forReason: .invalid))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.findFolder(forName: folderName) == nil {
|
||||||
|
completion(.unsupported(forReason: .invalid))
|
||||||
|
} else {
|
||||||
|
completion(.success(with: folderName))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func handle(intent: AddWebFeedIntent, completion: @escaping (AddWebFeedIntentResponse) -> Void) {
|
public func handle(intent: AddWebFeedIntent, completion: @escaping (AddWebFeedIntentResponse) -> Void) {
|
||||||
guard let url = intent.url else {
|
guard let url = intent.url, let extensionContainers = ExtensionContainersFile.read() else {
|
||||||
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
let account: ExtensionAccount? = {
|
||||||
|
if let accountName = intent.accountName {
|
||||||
let account: Account? = {
|
return extensionContainers.findAccount(forName: accountName)
|
||||||
if let accountName = intent.accountName {
|
} else {
|
||||||
return AccountManager.shared.findActiveAccount(forDisplayName: accountName)
|
return extensionContainers.accounts.first
|
||||||
} else {
|
|
||||||
return AccountManager.shared.sortedActiveAccounts.first
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
guard let validAccount = account else {
|
|
||||||
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
let container: Container? = {
|
guard let validAccount = account else {
|
||||||
if let folderName = intent.folderName {
|
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
||||||
return validAccount.findFolder(withDisplayName: folderName)
|
return
|
||||||
} else {
|
|
||||||
return validAccount
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
guard let validContainer = container else {
|
|
||||||
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
validAccount.createWebFeed(url: url.absoluteString, name: nil, container: validContainer) { result in
|
|
||||||
switch result {
|
|
||||||
case .success:
|
|
||||||
AccountManager.shared.suspendNetworkAll()
|
|
||||||
AccountManager.shared.suspendDatabaseAll()
|
|
||||||
completion(AddWebFeedIntentResponse(code: .success, userActivity: nil))
|
|
||||||
case .failure(let error):
|
|
||||||
switch error {
|
|
||||||
case AccountError.createErrorNotFound:
|
|
||||||
completion(AddWebFeedIntentResponse(code: .feedNotFound, userActivity: nil))
|
|
||||||
case AccountError.createErrorAlreadySubscribed:
|
|
||||||
completion(AddWebFeedIntentResponse(code: .alreadySubscribed, userActivity: nil))
|
|
||||||
default:
|
|
||||||
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let container: ExtensionContainer? = {
|
||||||
|
if let folderName = intent.folderName {
|
||||||
|
return validAccount.findFolder(forName: folderName)
|
||||||
|
} else {
|
||||||
|
return validAccount
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let validContainer = container, let containerID = validContainer.containerID else {
|
||||||
|
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = ExtensionFeedAddRequest(name: nil, feedURL: url, destinationContainerID: containerID)
|
||||||
|
ExtensionFeedAddRequestFile.save(request)
|
||||||
|
completion(AddWebFeedIntentResponse(code: .success, userActivity: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<key>INIntentDefinitionNamespace</key>
|
<key>INIntentDefinitionNamespace</key>
|
||||||
<string>U6u7RF</string>
|
<string>U6u7RF</string>
|
||||||
<key>INIntentDefinitionSystemVersion</key>
|
<key>INIntentDefinitionSystemVersion</key>
|
||||||
<string>19B88</string>
|
<string>19D76</string>
|
||||||
<key>INIntentDefinitionToolsBuildVersion</key>
|
<key>INIntentDefinitionToolsBuildVersion</key>
|
||||||
<string>11B53</string>
|
<string>11B53</string>
|
||||||
<key>INIntentDefinitionToolsVersion</key>
|
<key>INIntentDefinitionToolsVersion</key>
|
||||||
@ -177,6 +177,16 @@
|
|||||||
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
|
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
|
||||||
<string>JGkCuS</string>
|
<string>JGkCuS</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>INIntentParameterUnsupportedReasonCode</key>
|
||||||
|
<string>communication</string>
|
||||||
|
<key>INIntentParameterUnsupportedReasonCustom</key>
|
||||||
|
<true/>
|
||||||
|
<key>INIntentParameterUnsupportedReasonFormatString</key>
|
||||||
|
<string>Unable to communicate with NetNewsWire.</string>
|
||||||
|
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
|
||||||
|
<string>uSfloN</string>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
@ -259,6 +269,16 @@
|
|||||||
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
|
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
|
||||||
<string>ef5kBt</string>
|
<string>ef5kBt</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>INIntentParameterUnsupportedReasonCode</key>
|
||||||
|
<string>communication</string>
|
||||||
|
<key>INIntentParameterUnsupportedReasonCustom</key>
|
||||||
|
<true/>
|
||||||
|
<key>INIntentParameterUnsupportedReasonFormatString</key>
|
||||||
|
<string>Unable to communicate with NetNewsWire.</string>
|
||||||
|
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
|
||||||
|
<string>ExjqcE</string>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
@ -276,30 +296,6 @@
|
|||||||
<key>INIntentResponseCodeName</key>
|
<key>INIntentResponseCodeName</key>
|
||||||
<string>failure</string>
|
<string>failure</string>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
|
||||||
<key>INIntentResponseCodeConciseFormatString</key>
|
|
||||||
<string>You are already subscribed to this feed in this account.</string>
|
|
||||||
<key>INIntentResponseCodeConciseFormatStringID</key>
|
|
||||||
<string>srME8b</string>
|
|
||||||
<key>INIntentResponseCodeFormatString</key>
|
|
||||||
<string>You are already subscribed to this feed in this account.</string>
|
|
||||||
<key>INIntentResponseCodeFormatStringID</key>
|
|
||||||
<string>UGGPkp</string>
|
|
||||||
<key>INIntentResponseCodeName</key>
|
|
||||||
<string>alreadySubscribed</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>INIntentResponseCodeConciseFormatString</key>
|
|
||||||
<string>No feed was found at the specified URL.</string>
|
|
||||||
<key>INIntentResponseCodeConciseFormatStringID</key>
|
|
||||||
<string>8Dh9Yy</string>
|
|
||||||
<key>INIntentResponseCodeFormatString</key>
|
|
||||||
<string>No feed was found at the specified URL.</string>
|
|
||||||
<key>INIntentResponseCodeFormatStringID</key>
|
|
||||||
<string>drQfaI</string>
|
|
||||||
<key>INIntentResponseCodeName</key>
|
|
||||||
<string>feedNotFound</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<key>INIntentTitle</key>
|
<key>INIntentTitle</key>
|
||||||
|
@ -58,7 +58,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedMetadataDidChange(_:)), name: .WebFeedMetadataDidChange, object: nil)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||||
|
@ -60,10 +60,10 @@
|
|||||||
<string>Grant permission to save images from the article.</string>
|
<string>Grant permission to save images from the article.</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>Restoration</string>
|
|
||||||
<string>AddWebFeedIntent</string>
|
<string>AddWebFeedIntent</string>
|
||||||
<string>NextUnread</string>
|
<string>NextUnread</string>
|
||||||
<string>ReadArticle</string>
|
<string>ReadArticle</string>
|
||||||
|
<string>Restoration</string>
|
||||||
<string>SelectFeed</string>
|
<string>SelectFeed</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
49
iOS/ShareExtension/ShareDefaultContainer.swift
Normal file
49
iOS/ShareExtension/ShareDefaultContainer.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// ShareDefaultContainer.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 2/11/20.
|
||||||
|
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ShareDefaultContainer {
|
||||||
|
|
||||||
|
static func defaultContainer(containers: ExtensionContainers) -> ExtensionContainer? {
|
||||||
|
|
||||||
|
if let accountID = AppDefaults.addWebFeedAccountID, let account = containers.accounts.first(where: { $0.accountID == accountID }) {
|
||||||
|
if let folderName = AppDefaults.addWebFeedFolderName, let folder = account.folders.first(where: { $0.name == folderName }) {
|
||||||
|
return folder
|
||||||
|
} else {
|
||||||
|
return substituteContainerIfNeeded(account: account)
|
||||||
|
}
|
||||||
|
} else if let account = containers.accounts.first {
|
||||||
|
return substituteContainerIfNeeded(account: account)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveDefaultContainer(_ container: ExtensionContainer) {
|
||||||
|
AppDefaults.addWebFeedAccountID = container.accountID
|
||||||
|
if let folder = container as? ExtensionFolder {
|
||||||
|
AppDefaults.addWebFeedFolderName = folder.name
|
||||||
|
} else {
|
||||||
|
AppDefaults.addWebFeedFolderName = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func substituteContainerIfNeeded(account: ExtensionAccount) -> ExtensionContainer? {
|
||||||
|
if !account.disallowFeedInRootFolder {
|
||||||
|
return account
|
||||||
|
} else {
|
||||||
|
if let folder = account.folders.first {
|
||||||
|
return folder
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,30 +7,24 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import RSCore
|
|
||||||
import Account
|
import Account
|
||||||
|
import RSCore
|
||||||
|
|
||||||
protocol ShareFolderPickerControllerDelegate: class {
|
protocol ShareFolderPickerControllerDelegate: class {
|
||||||
func shareFolderPickerDidSelect(_ container: Container)
|
func shareFolderPickerDidSelect(_ container: ExtensionContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShareFolderPickerController: UITableViewController {
|
class ShareFolderPickerController: UITableViewController {
|
||||||
|
|
||||||
var selectedContainer: Container?
|
var containers: [ExtensionContainer]?
|
||||||
var containers = [Container]()
|
var selectedContainerID: ContainerIdentifier?
|
||||||
|
|
||||||
weak var delegate: ShareFolderPickerControllerDelegate?
|
weak var delegate: ShareFolderPickerControllerDelegate?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
for account in AccountManager.shared.sortedActiveAccounts {
|
|
||||||
containers.append(account)
|
|
||||||
if let sortedFolders = account.sortedFolders {
|
|
||||||
containers.append(contentsOf: sortedFolders)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "ShareFolderPickerAccountCell", bundle: Bundle.main), forCellReuseIdentifier: "AccountCell")
|
tableView.register(UINib(nibName: "ShareFolderPickerAccountCell", bundle: Bundle.main), forCellReuseIdentifier: "AccountCell")
|
||||||
tableView.register(UINib(nibName: "ShareFolderPickerFolderCell", bundle: Bundle.main), forCellReuseIdentifier: "FolderCell")
|
tableView.register(UINib(nibName: "ShareFolderPickerFolderCell", bundle: Bundle.main), forCellReuseIdentifier: "FolderCell")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
@ -38,30 +32,28 @@ class ShareFolderPickerController: UITableViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
return containers.count
|
return containers?.count ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let container = containers[indexPath.row]
|
let container = containers?[indexPath.row]
|
||||||
let cell: ShareFolderPickerCell = {
|
let cell: ShareFolderPickerCell = {
|
||||||
if container is Account {
|
if container is ExtensionAccount {
|
||||||
return tableView.dequeueReusableCell(withIdentifier: "AccountCell", for: indexPath) as! ShareFolderPickerCell
|
return tableView.dequeueReusableCell(withIdentifier: "AccountCell", for: indexPath) as! ShareFolderPickerCell
|
||||||
} else {
|
} else {
|
||||||
return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! ShareFolderPickerCell
|
return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! ShareFolderPickerCell
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if let account = container as? Account {
|
if let account = container as? ExtensionAccount {
|
||||||
cell.icon.image = AppAssets.image(for: account.type)
|
cell.icon.image = AppAssets.image(for: account.type)
|
||||||
} else {
|
} else {
|
||||||
cell.icon.image = AppAssets.masterFolderImage.image
|
cell.icon.image = AppAssets.masterFolderImage.image
|
||||||
}
|
}
|
||||||
|
|
||||||
if let displayNameProvider = container as? DisplayNameProvider {
|
cell.label?.text = container?.name ?? ""
|
||||||
cell.label?.text = displayNameProvider.nameForDisplay
|
|
||||||
}
|
|
||||||
|
|
||||||
if let compContainer = selectedContainer, container === compContainer {
|
if let containerID = container?.containerID, containerID == selectedContainerID {
|
||||||
cell.accessoryType = .checkmark
|
cell.accessoryType = .checkmark
|
||||||
} else {
|
} else {
|
||||||
cell.accessoryType = .none
|
cell.accessoryType = .none
|
||||||
@ -71,9 +63,9 @@ class ShareFolderPickerController: UITableViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
let container = containers[indexPath.row]
|
guard let container = containers?[indexPath.row] else { return }
|
||||||
|
|
||||||
if let account = container as? Account, account.behaviors.contains(.disallowFeedInRootFolder) {
|
if let account = container as? ExtensionAccount, account.disallowFeedInRootFolder {
|
||||||
tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
|
tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
|
||||||
} else {
|
} else {
|
||||||
let cell = tableView.cellForRow(at: indexPath)
|
let cell = tableView.cellForRow(at: indexPath)
|
||||||
|
@ -8,23 +8,26 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import Social
|
|
||||||
import Account
|
import Account
|
||||||
import Articles
|
import Social
|
||||||
import RSCore
|
import RSCore
|
||||||
import RSTree
|
import RSTree
|
||||||
|
|
||||||
class ShareViewController: SLComposeServiceViewController, ShareFolderPickerControllerDelegate {
|
class ShareViewController: SLComposeServiceViewController, ShareFolderPickerControllerDelegate {
|
||||||
|
|
||||||
private var url: URL?
|
private var url: URL?
|
||||||
private var container: Container?
|
private var extensionContainers: ExtensionContainers?
|
||||||
|
private var flattenedContainers: [ExtensionContainer]!
|
||||||
|
private var selectedContainer: ExtensionContainer?
|
||||||
private var folderItem: SLComposeSheetConfigurationItem!
|
private var folderItem: SLComposeSheetConfigurationItem!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
|
||||||
AccountManager.shared = AccountManager()
|
extensionContainers = ExtensionContainersFile.read()
|
||||||
|
flattenedContainers = extensionContainers?.flattened ?? [ExtensionContainer]()
|
||||||
container = AddWebFeedDefaultContainer.defaultContainer
|
if let extensionContainers = extensionContainers {
|
||||||
|
selectedContainer = ShareDefaultContainer.defaultContainer(containers: extensionContainers)
|
||||||
|
}
|
||||||
|
|
||||||
title = "NetNewsWire"
|
title = "NetNewsWire"
|
||||||
placeholder = "Feed Name (Optional)"
|
placeholder = "Feed Name (Optional)"
|
||||||
@ -91,53 +94,25 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func isContentValid() -> Bool {
|
override func isContentValid() -> Bool {
|
||||||
return url != nil && container != nil
|
return url != nil && selectedContainer != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didSelectPost() {
|
override func didSelectPost() {
|
||||||
|
guard let url = url, let selectedContainer = selectedContainer, let containerID = selectedContainer.containerID else {
|
||||||
guard let url = url, let container = container else {
|
|
||||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var account: Account?
|
let name = contentText.isEmpty ? nil : contentText
|
||||||
if let containerAccount = container as? Account {
|
let request = ExtensionFeedAddRequest(name: name, feedURL: url, destinationContainerID: containerID)
|
||||||
account = containerAccount
|
ExtensionFeedAddRequestFile.save(request)
|
||||||
} else if let containerFolder = container as? Folder, let containerAccount = containerFolder.account {
|
|
||||||
account = containerAccount
|
|
||||||
} else {
|
|
||||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if account!.hasWebFeed(withURL: url.absoluteString) {
|
|
||||||
let errorTitle = NSLocalizedString("Error", comment: "Error")
|
|
||||||
presentError(title: errorTitle, message: AccountError.createErrorAlreadySubscribed.localizedDescription)
|
|
||||||
self.extensionContext!.cancelRequest(withError: AccountError.createErrorAlreadySubscribed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let feedName = contentText.isEmpty ? nil : contentText
|
|
||||||
|
|
||||||
ProcessInfo.processInfo.performExpiringActivity(withReason: "Adding web feed to account.") { expired in
|
|
||||||
guard !expired else { return }
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
account!.createWebFeed(url: url.absoluteString, name: feedName, container: container) { result in
|
|
||||||
account!.save()
|
|
||||||
AccountManager.shared.suspendNetworkAll()
|
|
||||||
AccountManager.shared.suspendDatabaseAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shareFolderPickerDidSelect(_ container: Container) {
|
func shareFolderPickerDidSelect(_ container: ExtensionContainer) {
|
||||||
AddWebFeedDefaultContainer.saveDefaultContainer(container)
|
ShareDefaultContainer.saveDefaultContainer(container)
|
||||||
self.container = container
|
self.selectedContainer = container
|
||||||
updateFolderItemValue()
|
updateFolderItemValue()
|
||||||
self.popConfigurationViewController()
|
self.popConfigurationViewController()
|
||||||
}
|
}
|
||||||
@ -159,7 +134,8 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
|||||||
|
|
||||||
folderPickerController.navigationController?.title = NSLocalizedString("Folder", comment: "Folder")
|
folderPickerController.navigationController?.title = NSLocalizedString("Folder", comment: "Folder")
|
||||||
folderPickerController.delegate = self
|
folderPickerController.delegate = self
|
||||||
folderPickerController.selectedContainer = self.container
|
folderPickerController.containers = self.flattenedContainers
|
||||||
|
folderPickerController.selectedContainerID = self.selectedContainer?.containerID
|
||||||
|
|
||||||
self.pushConfigurationViewController(folderPickerController)
|
self.pushConfigurationViewController(folderPickerController)
|
||||||
|
|
||||||
@ -174,12 +150,10 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
|||||||
private extension ShareViewController {
|
private extension ShareViewController {
|
||||||
|
|
||||||
func updateFolderItemValue() {
|
func updateFolderItemValue() {
|
||||||
if let containerName = (container as? DisplayNameProvider)?.nameForDisplay {
|
if let account = selectedContainer as? ExtensionAccount {
|
||||||
if container is Folder {
|
self.folderItem.value = account.name
|
||||||
self.folderItem.value = "\(container?.account?.nameForDisplay ?? "") / \(containerName)"
|
} else if let folder = selectedContainer as? ExtensionFolder {
|
||||||
} else {
|
self.folderItem.value = "\(folder.accountName) / \(folder.name)"
|
||||||
self.folderItem.value = containerName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user