mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Move account files to the documents directory and out of the shared container. Issue #1784
This commit is contained in:
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 authorAvatarDownloader: AuthorAvatarDownloader!
|
||||
var webFeedIconDownloader: WebFeedIconDownloader!
|
||||
var extensionContainersFile: ExtensionContainersFile!
|
||||
var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile!
|
||||
|
||||
var unreadCount = 0 {
|
||||
didSet {
|
||||
@@ -58,7 +60,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
super.init()
|
||||
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(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
|
||||
@@ -97,6 +104,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
userNotificationManager = UserNotificationManager()
|
||||
|
||||
extensionContainersFile = ExtensionContainersFile()
|
||||
extensionFeedAddRequestFile = ExtensionFeedAddRequestFile()
|
||||
|
||||
syncTimer = ArticleStatusSyncTimer()
|
||||
|
||||
#if DEBUG
|
||||
@@ -132,6 +142,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
}
|
||||
|
||||
func prepareAccountsForBackground() {
|
||||
extensionFeedAddRequestFile.suspend()
|
||||
syncTimer?.invalidate()
|
||||
scheduleBackgroundFeedRefresh()
|
||||
syncArticleStatus()
|
||||
@@ -139,6 +150,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
}
|
||||
|
||||
func prepareAccountsForForeground() {
|
||||
extensionFeedAddRequestFile.resume()
|
||||
|
||||
if let lastRefresh = AppDefaults.lastRefresh {
|
||||
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
||||
|
||||
@@ -519,7 +519,14 @@ private extension WebViewController {
|
||||
|
||||
func reloadArticleImage() {
|
||||
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?) {
|
||||
|
||||
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 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 {
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
DispatchQueue.main.sync {
|
||||
AccountManager.shared = AccountManager()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
DispatchQueue.main.async {
|
||||
let accountNames = AccountManager.shared.activeAccounts.compactMap { $0.nameForDisplay }
|
||||
completion(accountNames, nil)
|
||||
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||
completion(nil, AddWebFeedIntentHandlerError.communicationFailure)
|
||||
return
|
||||
}
|
||||
|
||||
let accountNames = extensionContainers.accounts.map { $0.name }
|
||||
completion(accountNames, nil)
|
||||
}
|
||||
|
||||
public func resolveAccountName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedAccountNameResolutionResult) -> Void) {
|
||||
@@ -38,25 +50,32 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
||||
completion(AddWebFeedAccountNameResolutionResult.notRequired())
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if AccountManager.shared.findActiveAccount(forDisplayName: accountName) == nil {
|
||||
completion(.unsupported(forReason: .invalid))
|
||||
} else {
|
||||
completion(.success(with: accountName))
|
||||
}
|
||||
|
||||
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||
completion(.unsupported(forReason: .communication))
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
DispatchQueue.main.async {
|
||||
guard let accountName = intent.accountName, let account = AccountManager.shared.findActiveAccount(forDisplayName: accountName) else {
|
||||
completion([String](), nil)
|
||||
return
|
||||
}
|
||||
|
||||
let folderNames = account.folders?.map { $0.nameForDisplay }
|
||||
completion(folderNames, nil)
|
||||
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||
completion(nil, AddWebFeedIntentHandlerError.communicationFailure)
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -65,73 +84,60 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let account = AccountManager.shared.findActiveAccount(forDisplayName: accountName) else {
|
||||
completion(.unsupported(forReason: .invalid))
|
||||
return
|
||||
}
|
||||
if account.findFolder(withDisplayName: folderName) == nil {
|
||||
completion(.unsupported(forReason: .invalid))
|
||||
} else {
|
||||
completion(.success(with: folderName))
|
||||
}
|
||||
guard let extensionContainers = ExtensionContainersFile.read() else {
|
||||
completion(.unsupported(forReason: .communication))
|
||||
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) {
|
||||
guard let url = intent.url else {
|
||||
guard let url = intent.url, let extensionContainers = ExtensionContainersFile.read() else {
|
||||
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
let account: Account? = {
|
||||
if let accountName = intent.accountName {
|
||||
return AccountManager.shared.findActiveAccount(forDisplayName: accountName)
|
||||
} else {
|
||||
return AccountManager.shared.sortedActiveAccounts.first
|
||||
}
|
||||
}()
|
||||
|
||||
guard let validAccount = account else {
|
||||
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
||||
return
|
||||
}
|
||||
|
||||
let container: Container? = {
|
||||
if let folderName = intent.folderName {
|
||||
return validAccount.findFolder(withDisplayName: folderName)
|
||||
} else {
|
||||
return validAccount
|
||||
}
|
||||
}()
|
||||
|
||||
guard let validContainer = container else {
|
||||
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
||||
return
|
||||
let account: ExtensionAccount? = {
|
||||
if let accountName = intent.accountName {
|
||||
return extensionContainers.findAccount(forName: accountName)
|
||||
} else {
|
||||
return extensionContainers.accounts.first
|
||||
}
|
||||
}()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
guard let validAccount = account else {
|
||||
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
<string>U6u7RF</string>
|
||||
<key>INIntentDefinitionSystemVersion</key>
|
||||
<string>19B88</string>
|
||||
<string>19D76</string>
|
||||
<key>INIntentDefinitionToolsBuildVersion</key>
|
||||
<string>11B53</string>
|
||||
<key>INIntentDefinitionToolsVersion</key>
|
||||
@@ -177,6 +177,16 @@
|
||||
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
|
||||
<string>JGkCuS</string>
|
||||
</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>
|
||||
</dict>
|
||||
<dict>
|
||||
@@ -259,6 +269,16 @@
|
||||
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
|
||||
<string>ef5kBt</string>
|
||||
</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>
|
||||
</dict>
|
||||
</array>
|
||||
@@ -276,30 +296,6 @@
|
||||
<key>INIntentResponseCodeName</key>
|
||||
<string>failure</string>
|
||||
</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>
|
||||
</dict>
|
||||
<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(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, 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(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, 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>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>Restoration</string>
|
||||
<string>AddWebFeedIntent</string>
|
||||
<string>NextUnread</string>
|
||||
<string>ReadArticle</string>
|
||||
<string>Restoration</string>
|
||||
<string>SelectFeed</string>
|
||||
</array>
|
||||
<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 RSCore
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
protocol ShareFolderPickerControllerDelegate: class {
|
||||
func shareFolderPickerDidSelect(_ container: Container)
|
||||
func shareFolderPickerDidSelect(_ container: ExtensionContainer)
|
||||
}
|
||||
|
||||
class ShareFolderPickerController: UITableViewController {
|
||||
|
||||
var selectedContainer: Container?
|
||||
var containers = [Container]()
|
||||
var containers: [ExtensionContainer]?
|
||||
var selectedContainerID: ContainerIdentifier?
|
||||
|
||||
weak var delegate: ShareFolderPickerControllerDelegate?
|
||||
|
||||
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: "ShareFolderPickerFolderCell", bundle: Bundle.main), forCellReuseIdentifier: "FolderCell")
|
||||
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
@@ -38,30 +32,28 @@ class ShareFolderPickerController: UITableViewController {
|
||||
}
|
||||
|
||||
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 {
|
||||
let container = containers[indexPath.row]
|
||||
let container = containers?[indexPath.row]
|
||||
let cell: ShareFolderPickerCell = {
|
||||
if container is Account {
|
||||
if container is ExtensionAccount {
|
||||
return tableView.dequeueReusableCell(withIdentifier: "AccountCell", for: indexPath) as! ShareFolderPickerCell
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
cell.icon.image = AppAssets.masterFolderImage.image
|
||||
}
|
||||
|
||||
if let displayNameProvider = container as? DisplayNameProvider {
|
||||
cell.label?.text = displayNameProvider.nameForDisplay
|
||||
}
|
||||
|
||||
if let compContainer = selectedContainer, container === compContainer {
|
||||
|
||||
cell.label?.text = container?.name ?? ""
|
||||
|
||||
if let containerID = container?.containerID, containerID == selectedContainerID {
|
||||
cell.accessoryType = .checkmark
|
||||
} else {
|
||||
cell.accessoryType = .none
|
||||
@@ -71,9 +63,9 @@ class ShareFolderPickerController: UITableViewController {
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
let cell = tableView.cellForRow(at: indexPath)
|
||||
|
||||
@@ -8,23 +8,26 @@
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import Social
|
||||
import Account
|
||||
import Articles
|
||||
import Social
|
||||
import RSCore
|
||||
import RSTree
|
||||
|
||||
class ShareViewController: SLComposeServiceViewController, ShareFolderPickerControllerDelegate {
|
||||
|
||||
private var url: URL?
|
||||
private var container: Container?
|
||||
private var extensionContainers: ExtensionContainers?
|
||||
private var flattenedContainers: [ExtensionContainer]!
|
||||
private var selectedContainer: ExtensionContainer?
|
||||
private var folderItem: SLComposeSheetConfigurationItem!
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
AccountManager.shared = AccountManager()
|
||||
|
||||
container = AddWebFeedDefaultContainer.defaultContainer
|
||||
extensionContainers = ExtensionContainersFile.read()
|
||||
flattenedContainers = extensionContainers?.flattened ?? [ExtensionContainer]()
|
||||
if let extensionContainers = extensionContainers {
|
||||
selectedContainer = ShareDefaultContainer.defaultContainer(containers: extensionContainers)
|
||||
}
|
||||
|
||||
title = "NetNewsWire"
|
||||
placeholder = "Feed Name (Optional)"
|
||||
@@ -32,14 +35,14 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
button.title = "Add Feed"
|
||||
button.isEnabled = true
|
||||
}
|
||||
|
||||
|
||||
// Hack the bottom table rows to be smaller since the controller itself doesn't have enough sense to size itself correctly
|
||||
if let nav = self.children.first as? UINavigationController, let tableView = nav.children.first?.view.subviews.first as? UITableView {
|
||||
tableView.rowHeight = 38
|
||||
}
|
||||
|
||||
var provider: NSItemProvider? = nil
|
||||
|
||||
|
||||
// Try to get any HTML that is maybe passed in
|
||||
for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
|
||||
for itemProvider in item.attachments! {
|
||||
@@ -48,7 +51,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if provider != nil {
|
||||
provider!.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil, completionHandler: { [weak self] (pList, error) in
|
||||
if error != nil {
|
||||
@@ -66,7 +69,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Try to get the URL if it is passed in
|
||||
for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
|
||||
for itemProvider in item.attachments! {
|
||||
@@ -75,7 +78,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if provider != nil {
|
||||
provider!.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { [weak self] (urlCoded, error) in
|
||||
if error != nil {
|
||||
@@ -91,53 +94,25 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
}
|
||||
|
||||
override func isContentValid() -> Bool {
|
||||
return url != nil && container != nil
|
||||
return url != nil && selectedContainer != nil
|
||||
}
|
||||
|
||||
override func didSelectPost() {
|
||||
|
||||
guard let url = url, let container = container else {
|
||||
guard let url = url, let selectedContainer = selectedContainer, let containerID = selectedContainer.containerID else {
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
return
|
||||
}
|
||||
|
||||
var account: Account?
|
||||
if let containerAccount = container as? Account {
|
||||
account = containerAccount
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let name = contentText.isEmpty ? nil : contentText
|
||||
let request = ExtensionFeedAddRequest(name: name, feedURL: url, destinationContainerID: containerID)
|
||||
ExtensionFeedAddRequestFile.save(request)
|
||||
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
|
||||
func shareFolderPickerDidSelect(_ container: Container) {
|
||||
AddWebFeedDefaultContainer.saveDefaultContainer(container)
|
||||
self.container = container
|
||||
func shareFolderPickerDidSelect(_ container: ExtensionContainer) {
|
||||
ShareDefaultContainer.saveDefaultContainer(container)
|
||||
self.selectedContainer = container
|
||||
updateFolderItemValue()
|
||||
self.popConfigurationViewController()
|
||||
}
|
||||
@@ -159,7 +134,8 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
|
||||
folderPickerController.navigationController?.title = NSLocalizedString("Folder", comment: "Folder")
|
||||
folderPickerController.delegate = self
|
||||
folderPickerController.selectedContainer = self.container
|
||||
folderPickerController.containers = self.flattenedContainers
|
||||
folderPickerController.selectedContainerID = self.selectedContainer?.containerID
|
||||
|
||||
self.pushConfigurationViewController(folderPickerController)
|
||||
|
||||
@@ -174,12 +150,10 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
private extension ShareViewController {
|
||||
|
||||
func updateFolderItemValue() {
|
||||
if let containerName = (container as? DisplayNameProvider)?.nameForDisplay {
|
||||
if container is Folder {
|
||||
self.folderItem.value = "\(container?.account?.nameForDisplay ?? "") / \(containerName)"
|
||||
} else {
|
||||
self.folderItem.value = containerName
|
||||
}
|
||||
if let account = selectedContainer as? ExtensionAccount {
|
||||
self.folderItem.value = account.name
|
||||
} else if let folder = selectedContainer as? ExtensionFolder {
|
||||
self.folderItem.value = "\(folder.accountName) / \(folder.name)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user