mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2024-12-18 04:20:39 +01:00
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 AccountStateDidChange = Notification.Name(rawValue: "AccountStateDidChange")
|
||||
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.
|
||||
case onMyMac = 1
|
||||
case feedly = 16
|
||||
@ -199,8 +198,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
typealias WebFeedMetadataDictionary = [String: WebFeedMetadata]
|
||||
var webFeedMetadata = WebFeedMetadataDictionary()
|
||||
|
||||
var startingUp = true
|
||||
|
||||
public var unreadCount = 0 {
|
||||
didSet {
|
||||
if unreadCount != oldValue {
|
||||
@ -287,7 +284,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
}
|
||||
|
||||
self.delegate.accountDidInitialize(self)
|
||||
startingUp = false
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
@ -416,9 +412,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
public func suspendDatabase() {
|
||||
database.cancelAndSuspend()
|
||||
save()
|
||||
metadataFile.suspend()
|
||||
webFeedMetadataFile.suspend()
|
||||
opmlFile.suspend()
|
||||
}
|
||||
|
||||
/// Re-open the SQLite database and allow database calls.
|
||||
@ -430,12 +423,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
|
||||
/// Reload OPML, etc.
|
||||
public func resume() {
|
||||
metadataFile.resume()
|
||||
webFeedMetadataFile.resume()
|
||||
opmlFile.resume()
|
||||
metadataFile.load()
|
||||
webFeedMetadataFile.load()
|
||||
opmlFile.load()
|
||||
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>? {
|
||||
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() {
|
||||
// Feeds were added or deleted. Or folders added or deleted.
|
||||
// Or feeds inside folders were added or deleted.
|
||||
if !startingUp {
|
||||
opmlFile.markAsDirty()
|
||||
}
|
||||
opmlFile.markAsDirty()
|
||||
flattenedWebFeedsNeedUpdate = true
|
||||
webFeedDictionaryNeedsUpdate = true
|
||||
}
|
||||
|
@ -90,14 +90,6 @@ public final class AccountManager: UnreadCountProvider {
|
||||
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) {
|
||||
self.accountsFolder = accountsFolder
|
||||
|
||||
|
@ -16,41 +16,26 @@ final class AccountMetadataFile {
|
||||
|
||||
private let fileURL: URL
|
||||
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) {
|
||||
self.fileURL = URL(fileURLWithPath: filename)
|
||||
self.account = account
|
||||
}
|
||||
|
||||
func markAsDirty() {
|
||||
managedFile.markAsDirty()
|
||||
isDirty = true
|
||||
}
|
||||
|
||||
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 fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
||||
let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
||||
if let fileData = try? Data(contentsOf: readURL) {
|
||||
@ -63,17 +48,16 @@ private extension AccountMetadataFile {
|
||||
if let error = errorPointer?.pointee {
|
||||
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func saveCallback() {
|
||||
func save() {
|
||||
guard !account.isDeleted else { return }
|
||||
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .binary
|
||||
|
||||
let errorPointer: NSErrorPointer = nil
|
||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
||||
let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
||||
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 }
|
||||
}
|
||||
|
||||
public enum ContainerIdentifier: Hashable {
|
||||
public enum ContainerIdentifier: Hashable, Equatable {
|
||||
case smartFeedController
|
||||
case account(String) // accountID
|
||||
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,62 +17,40 @@ final class OPMLFile {
|
||||
|
||||
private let fileURL: URL
|
||||
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) {
|
||||
self.fileURL = URL(fileURLWithPath: filename)
|
||||
self.account = account
|
||||
}
|
||||
|
||||
func markAsDirty() {
|
||||
managedFile.markAsDirty()
|
||||
isDirty = true
|
||||
}
|
||||
|
||||
func load() {
|
||||
managedFile.load()
|
||||
}
|
||||
|
||||
func save() {
|
||||
managedFile.saveIfNecessary()
|
||||
}
|
||||
|
||||
func suspend() {
|
||||
managedFile.suspend()
|
||||
}
|
||||
|
||||
func resume() {
|
||||
managedFile.resume()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension OPMLFile {
|
||||
|
||||
func loadCallback() {
|
||||
guard let fileData = opmlFileData() else {
|
||||
guard let fileData = opmlFileData(), let opmlItems = parsedOPMLItems(fileData: fileData) else {
|
||||
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 {
|
||||
account.topLevelWebFeeds.removeAll()
|
||||
account.loadOPMLItems(opmlItems, parentFolder: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func saveCallback() {
|
||||
func save() {
|
||||
guard !account.isDeleted else { return }
|
||||
|
||||
let opmlDocumentString = opmlDocument()
|
||||
|
||||
let errorPointer: NSErrorPointer = nil
|
||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
||||
let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
||||
do {
|
||||
@ -85,12 +63,28 @@ private extension OPMLFile {
|
||||
if let error = errorPointer?.pointee {
|
||||
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? {
|
||||
var fileData: Data? = nil
|
||||
let errorPointer: NSErrorPointer = nil
|
||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
||||
let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
||||
do {
|
||||
|
@ -16,41 +16,26 @@ final class WebFeedMetadataFile {
|
||||
|
||||
private let fileURL: URL
|
||||
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) {
|
||||
self.fileURL = URL(fileURLWithPath: filename)
|
||||
self.account = account
|
||||
}
|
||||
|
||||
func markAsDirty() {
|
||||
managedFile.markAsDirty()
|
||||
isDirty = true
|
||||
}
|
||||
|
||||
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 fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
||||
let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
|
||||
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.values.forEach { $0.delegate = account }
|
||||
if !account.startingUp {
|
||||
account.resetWebFeedMetadataAndUnreadCounts()
|
||||
}
|
||||
})
|
||||
|
||||
if let error = errorPointer?.pointee {
|
||||
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func saveCallback() {
|
||||
func save() {
|
||||
guard !account.isDeleted else { return }
|
||||
|
||||
let feedMetadata = metadataForOnlySubscribedToFeeds()
|
||||
@ -79,7 +59,7 @@ private extension WebFeedMetadataFile {
|
||||
encoder.outputFormat = .binary
|
||||
|
||||
let errorPointer: NSErrorPointer = nil
|
||||
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
|
||||
let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
|
||||
do {
|
||||
@ -94,7 +74,22 @@ private extension WebFeedMetadataFile {
|
||||
os_log(.error, log: log, "Save to disk coordination failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension WebFeedMetadataFile {
|
||||
|
||||
func queueSaveToDiskIfNeeded() {
|
||||
saveQueue.add(self, #selector(saveToDiskIfNeeded))
|
||||
}
|
||||
|
||||
@objc func saveToDiskIfNeeded() {
|
||||
if isDirty {
|
||||
isDirty = false
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
private func metadataForOnlySubscribedToFeeds() -> Account.WebFeedMetadataDictionary {
|
||||
let webFeedIDs = account.idToWebFeedDictionary.keys
|
||||
return account.webFeedMetadata.filter { (feedID: String, metadata: WebFeedMetadata) -> Bool in
|
||||
|
@ -207,7 +207,14 @@ private extension DetailWebViewController {
|
||||
|
||||
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 reloadHTML() {
|
||||
|
@ -137,7 +137,6 @@
|
||||
51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; };
|
||||
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.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 */; };
|
||||
51A9A5E42380C8880033AADF /* ShareFolderPickerAccountCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.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 */; };
|
||||
51A9A60A2382FD240033AADF /* PoppableGestureRecognizerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.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 */; };
|
||||
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
|
||||
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 */; };
|
||||
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
|
||||
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 */; };
|
||||
51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1793,6 +1812,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
513C5CE8232571C2003D4054 /* ShareViewController.swift */,
|
||||
51B5C8BC23F3780900032075 /* ShareDefaultContainer.swift */,
|
||||
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */,
|
||||
51A9A5E72380CA130033AADF /* ShareFolderPickerCell.swift */,
|
||||
51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */,
|
||||
@ -1881,6 +1901,17 @@
|
||||
path = Activity;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2612,6 +2643,7 @@
|
||||
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */,
|
||||
C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */,
|
||||
51B62E67233186730085F949 /* IconView.swift */,
|
||||
51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */,
|
||||
51C4525D226508F600C03939 /* MasterFeed */,
|
||||
51C4526D2265091600C03939 /* MasterTimeline */,
|
||||
51C4527D2265092C00C03939 /* Article */,
|
||||
@ -2621,6 +2653,7 @@
|
||||
513145F9235A55A700387FDC /* Intents */,
|
||||
5183CCEB227117C70010922C /* Settings */,
|
||||
51C45245226506C800C03939 /* UIKit Extensions */,
|
||||
51B5C85A23F22A7A00032075 /* CommonExtension */,
|
||||
513C5CE7232571C2003D4054 /* ShareExtension */,
|
||||
51314643235A7C2300387FDC /* IntentsExtension */,
|
||||
84C9FC9A2262A1A900D921D6 /* Resources */,
|
||||
@ -3644,8 +3677,12 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
513146B3235A81A400387FDC /* AddWebFeedIntentHandler.swift in Sources */,
|
||||
51B5C8E623F4BBFA00032075 /* ExtensionFeedAddRequest.swift in Sources */,
|
||||
51314705235C41FC00387FDC /* Intents.intentdefinition in Sources */,
|
||||
51B5C8E523F4BBFA00032075 /* ExtensionContainersFile.swift in Sources */,
|
||||
51314668235A7E4600387FDC /* IntentHandler.swift in Sources */,
|
||||
51B5C8E423F4BBFA00032075 /* ExtensionContainers.swift in Sources */,
|
||||
51B5C8E723F4BBFA00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -3654,13 +3691,17 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */,
|
||||
51B5C8B923F368D000032075 /* ExtensionContainers.swift in Sources */,
|
||||
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */,
|
||||
513C5CE9232571C2003D4054 /* ShareViewController.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 */,
|
||||
51A9A5EF2380D63B0033AADF /* IconImage.swift in Sources */,
|
||||
51A9A5ED2380D6000033AADF /* AppAssets.swift in Sources */,
|
||||
51B5C8C123F3A0DB00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
|
||||
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -3899,9 +3940,11 @@
|
||||
51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */,
|
||||
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
|
||||
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */,
|
||||
51B5C87723F22B8200032075 /* ExtensionContainers.swift in Sources */,
|
||||
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */,
|
||||
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
|
||||
514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */,
|
||||
51B5C87B23F2317700032075 /* ExtensionFeedAddRequest.swift in Sources */,
|
||||
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */,
|
||||
512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */,
|
||||
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
|
||||
@ -3925,6 +3968,7 @@
|
||||
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
|
||||
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */,
|
||||
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */,
|
||||
51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */,
|
||||
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
|
||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
|
||||
C5A6ED6D23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift in Sources */,
|
||||
@ -3949,8 +3993,10 @@
|
||||
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */,
|
||||
5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */,
|
||||
51C452882265093600C03939 /* AddWebFeedViewController.swift in Sources */,
|
||||
51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
|
||||
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */,
|
||||
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
|
||||
51C9DE8623F09FAA003D5A6D /* AccountMigrator.swift in Sources */,
|
||||
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */,
|
||||
518651DA235621840078E021 /* ImageTransition.swift in Sources */,
|
||||
51C266EA238C334800F53014 /* ContextMenuPreviewViewController.swift in Sources */,
|
||||
|
@ -79,9 +79,9 @@ function flattenPreElements() {
|
||||
ElementUnwrapper.unwrapAppropriateChildren("div.articleBody td > pre");
|
||||
}
|
||||
|
||||
function reloadArticleImage(articleID) {
|
||||
function reloadArticleImage(imageSrc) {
|
||||
var image = document.getElementById("nnwImageIcon");
|
||||
image.src = "nnwImageIcon://" + articleID;
|
||||
image.src = imageSrc;
|
||||
}
|
||||
|
||||
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 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)"
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user