This commit is contained in:
Brent Simmons 2019-05-17 23:02:57 -07:00
commit abad2ca8ab
15 changed files with 428 additions and 148 deletions

View File

@ -36,9 +36,25 @@ public enum AccountType: Int {
// TODO: more
}
public enum AccountError: Error {
public enum AccountError: LocalizedError {
case createErrorNotFound
case createErrorAlreadySubscribed
case opmlImportInProgress
public var errorDescription: String? {
switch self {
case .opmlImportInProgress:
return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running")
default:
return NSLocalizedString("An unknown error occurred.", comment: "Unknown error")
}
}
public var recoverySuggestion: String? {
return NSLocalizedString("Please try again later.", comment: "Try later")
}
}
public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
@ -299,10 +315,18 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
public func importOPML(_ opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
guard !delegate.opmlImportInProgress else {
completion(.failure(AccountError.opmlImportInProgress))
return
}
delegate.importOPML(for: self, opmlFile: opmlFile) { [weak self] result in
switch result {
case .success:
guard let self = self else { return }
// Reset the last fetch date to get the article history for the added feeds.
self.metadata.lastArticleFetch = nil
self.delegate.refreshAll(for: self) {
completion(.success(()))
}
@ -310,6 +334,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
completion(.failure(error))
}
}
}
public func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
@ -544,6 +569,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return database.fetchStarredArticleIDs()
}
public func fetchArticleIDsForStatusesWithoutArticles() -> Set<String> {
return database.fetchArticleIDsForStatusesWithoutArticles()
}
public func opmlDocument() -> String {
let escapedTitle = nameForDisplay.rs_stringByEscapingSpecialXMLCharacters()
let openingText =

View File

@ -17,6 +17,7 @@
5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133230F22810E5700C30F19 /* FeedbinIcon.swift */; };
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; };
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; };
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */; };
5165D7122282080C00D9D53D /* AccountFolderContentsSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */; };
5165D71622821C2400D9D53D /* taggings_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71322821C2400D9D53D /* taggings_delete.json */; };
5165D71722821C2400D9D53D /* taggings_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71422821C2400D9D53D /* taggings_add.json */; };
@ -116,6 +117,7 @@
5133230F22810E5700C30F19 /* FeedbinIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinIcon.swift; sourceTree = "<group>"; };
5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = "<group>"; };
5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = "<group>"; };
5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinImportResult.swift; sourceTree = "<group>"; };
5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderContentsSyncTest.swift; sourceTree = "<group>"; };
5165D71322821C2400D9D53D /* taggings_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_delete.json; sourceTree = "<group>"; };
5165D71422821C2400D9D53D /* taggings_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_add.json; sourceTree = "<group>"; };
@ -255,6 +257,7 @@
51E490352288C37100C791F0 /* FeedbinDate.swift */,
84CAD7151FDF2E22000F0755 /* FeedbinEntry.swift */,
5133230F22810E5700C30F19 /* FeedbinIcon.swift */,
5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */,
51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */,
84245C841FDDD8CB0074AFBB /* FeedbinSubscription.swift */,
51D58754227F53BE00900287 /* FeedbinTag.swift */,
@ -521,6 +524,7 @@
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */,
844B297D2106C7EC004020B3 /* Feed.swift in Sources */,
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */,
84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */,
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,

View File

@ -14,6 +14,8 @@ protocol AccountDelegate {
// Local account does not; some synced accounts might.
var supportsSubFolders: Bool { get }
var opmlImportInProgress: Bool { get }
var server: String? { get }
var credentials: Credentials? { get set }
var accountMetadata: AccountMetadata? { get set }

View File

@ -67,6 +67,54 @@ final class FeedbinAPICaller: NSObject {
}
func importOPML(opmlData: Data, completion: @escaping (Result<FeedbinImportResult, Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("imports.json")
let request = URLRequest(url: callURL, credentials: credentials)
transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in
switch result {
case .success(let (_, data)):
guard let resultData = data else {
completion(.failure(TransportError.noData))
break
}
do {
let result = try JSONDecoder().decode(FeedbinImportResult.self, from: resultData)
completion(.success(result))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result<FeedbinImportResult?, Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("imports/\(importID).json")
let request = URLRequest(url: callURL, credentials: credentials)
transport.send(request: request, resultType: FeedbinImportResult.self) { result in
switch result {
case .success(let (_, importResult)):
completion(.success(importResult))
case .failure(let error):
completion(.failure(error))
}
}
}
func retrieveTags(completion: @escaping (Result<[FeedbinTag]?, Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("tags.json")
@ -299,6 +347,33 @@ final class FeedbinAPICaller: NSObject {
}
func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([FeedbinEntry]?), Error>) -> Void) {
guard !articleIDs.isEmpty else {
completion(.success(([FeedbinEntry]())))
return
}
let concatIDs = articleIDs.reduce("") { param, articleID in return param + ",\(articleID)" }
let paramIDs = String(concatIDs.dropFirst())
var callURL = URLComponents(url: feedbinBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)!
callURL.queryItems = [URLQueryItem(name: "ids", value: paramIDs)]
let request = URLRequest(url: callURL.url!, credentials: credentials)
transport.send(request: request, resultType: [FeedbinEntry].self) { [weak self] result in
switch result {
case .success(let (_, entries)):
completion(.success((entries)))
case .failure(let error):
completion(.failure(error))
}
}
}
func retrieveEntries(feedID: String, completion: @escaping (Result<([FeedbinEntry]?, String?), Error>) -> Void) {
let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()
@ -359,25 +434,6 @@ final class FeedbinAPICaller: NSObject {
}
func extractPageNumber(link: String?) -> Int? {
guard let link = link else {
return nil
}
if let lowerBound = link.range(of: "page=")?.upperBound {
if let upperBound = link.range(of: "&")?.lowerBound {
return Int(link[lowerBound..<upperBound])
}
if let upperBound = link.range(of: ">")?.lowerBound {
return Int(link[lowerBound..<upperBound])
}
}
return nil
}
func retrieveEntries(page: String, completion: @escaping (Result<([FeedbinEntry]?, String?), Error>) -> Void) {
guard let callURL = URL(string: page) else {
@ -484,4 +540,23 @@ extension FeedbinAPICaller {
}
}
func extractPageNumber(link: String?) -> Int? {
guard let link = link else {
return nil
}
if let lowerBound = link.range(of: "page=")?.upperBound {
if let upperBound = link.range(of: "&")?.lowerBound {
return Int(link[lowerBound..<upperBound])
}
if let upperBound = link.range(of: ">")?.lowerBound {
return Int(link[lowerBound..<upperBound])
}
}
return nil
}
}

View File

@ -32,6 +32,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
let supportsSubFolders = false
let server: String? = "api.feedbin.com"
var opmlImportInProgress = false
var credentials: Credentials? {
didSet {
@ -79,7 +80,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
func refreshAll(for account: Account, completion: (() -> Void)? = nil) {
refreshProgress.addToNumberOfTasksAndRemaining(5)
refreshProgress.addToNumberOfTasksAndRemaining(6)
refreshAccount(account) { [weak self] result in
switch result {
@ -87,8 +88,12 @@ final class FeedbinAccountDelegate: AccountDelegate {
self?.refreshArticles(account) {
self?.refreshArticleStatus(for: account) {
self?.refreshProgress.clear()
completion?()
self?.refreshMissingArticles(account) {
self?.refreshProgress.clear()
DispatchQueue.main.async {
completion?()
}
}
}
}
@ -200,25 +205,32 @@ final class FeedbinAccountDelegate: AccountDelegate {
return
}
let parserData = ParserData(url: opmlFile.absoluteString, data: opmlData)
var opmlDocument: RSOPMLDocument?
os_log(.debug, log: log, "Begin importing OPML...")
opmlImportInProgress = true
do {
opmlDocument = try RSOPMLParser.parseOPML(with: parserData)
} catch {
completion(.failure(error))
return
caller.importOPML(opmlData: opmlData) { [weak self] result in
switch result {
case .success(let importResult):
if importResult.complete {
guard let self = self else { return }
os_log(.debug, log: self.log, "Import OPML done.")
self.opmlImportInProgress = false
DispatchQueue.main.async {
completion(.success(()))
}
} else {
self?.checkImportResult(opmlImportResultID: importResult.importResultID, completion: completion)
}
case .failure(let error):
guard let self = self else { return }
os_log(.debug, log: self.log, "Import OPML failed.")
self.opmlImportInProgress = false
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
guard let loadDocument = opmlDocument, let children = loadDocument.children else {
completion(.success(()))
return
}
importOPMLItems(account, items: children, parentFolder: nil)
completion(.success(()))
}
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
@ -507,6 +519,43 @@ private extension FeedbinAccountDelegate {
}
func checkImportResult(opmlImportResultID: Int, completion: @escaping (Result<Void, Error>) -> Void) {
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { [weak self] timer in
guard let self = self else { return }
os_log(.debug, log: self.log, "Checking status of OPML import...")
self.caller.retrieveOPMLImportResult(importID: opmlImportResultID) { result in
switch result {
case .success(let importResult):
if let result = importResult, result.complete {
os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.")
timer.invalidate()
self.opmlImportInProgress = false
DispatchQueue.main.async {
completion(.success(()))
}
}
case .failure(let error):
os_log(.debug, log: self.log, "Import OPML check failed.")
timer.invalidate()
self.opmlImportInProgress = false
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
}
}
func syncFolders(_ account: Account, _ tags: [FeedbinTag]?) {
guard let tags = tags else { return }
@ -749,125 +798,30 @@ private extension FeedbinAccountDelegate {
return
}
let group = DispatchGroup()
let articleIDs = statuses.compactMap { Int($0.articleID) }
let articleIDGroups = articleIDs.chunked(into: 1000)
for articleIDGroup in articleIDGroups {
group.enter()
apiCall(articleIDGroup) { [weak self] result in
switch result {
case .success:
self?.database.deleteSelectedForProcessing(articleIDGroup.map { String($0) } )
completion()
group.leave()
case .failure(let error):
guard let self = self else { return }
os_log(.error, log: self.log, "Article status sync call failed: %@.", error.localizedDescription)
self.database.resetSelectedForProcessing(articleIDGroup.map { String($0) } )
completion()
}
}
}
}
func importOPMLItems(_ account: Account, items: [RSOPMLItem], parentFolder: Folder?) {
items.forEach { (item) in
if let feedSpecifier = item.feedSpecifier {
importFeedSpecifier(account, feedSpecifier: feedSpecifier, parentFolder: parentFolder)
return
}
guard let folderName = item.titleFromAttributes else {
// Folder doesnt have a name, so it wont be created, and its items will go one level up.
if let itemChildren = item.children {
importOPMLItems(account, items: itemChildren, parentFolder: parentFolder)
}
return
}
if let folder = account.ensureFolder(with: folderName) {
if let itemChildren = item.children {
importOPMLItems(account, items: itemChildren, parentFolder: folder)
group.leave()
}
}
}
}
func importFeedSpecifier(_ account: Account, feedSpecifier: RSOPMLFeedSpecifier, parentFolder: Folder?) {
caller.createSubscription(url: feedSpecifier.feedURL) { [weak self] result in
switch result {
case .success(let subResult):
switch subResult {
case .created(let sub):
DispatchQueue.main.async {
let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL)
feed.subscriptionID = String(sub.subscriptionID)
self?.importFeedSpecifierPostProcess(account: account, sub: sub, feedSpecifier: feedSpecifier, feed: feed, parentFolder: parentFolder)
}
default:
break
}
case .failure(let error):
guard let self = self else { return }
os_log(.error, log: self.log, "Create feed on OPML import failed: %@.", error.localizedDescription)
}
}
}
func importFeedSpecifierPostProcess(account: Account, sub: FeedbinSubscription, feedSpecifier: RSOPMLFeedSpecifier, feed: Feed, parentFolder: Folder?) {
// Rename the feed if its name in the OPML file doesn't match the found name
if sub.name != feedSpecifier.title, let newName = feedSpecifier.title {
self.caller.renameSubscription(subscriptionID: String(sub.subscriptionID), newName: newName) { [weak self] result in
switch result {
case .success:
DispatchQueue.main.async {
feed.editedName = newName
}
case .failure(let error):
guard let self = self else { return }
os_log(.error, log: self.log, "Rename feed on OPML import failed: %@.", error.localizedDescription)
}
}
}
// Move the new feed if it is in a folder
if let folder = parentFolder, let feedID = Int(feed.feedID) {
self.caller.createTagging(feedID: feedID, name: folder.name ?? "") { [weak self] result in
switch result {
case .success(let taggingID):
DispatchQueue.main.async {
self?.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID))
folder.addFeed(feed)
}
case .failure(let error):
guard let self = self else { return }
os_log(.error, log: self.log, "Move feed to folder on OPML import failed: %@.", error.localizedDescription)
}
}
} else {
DispatchQueue.main.async {
account.addFeed(feed)
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
@ -1049,6 +1003,46 @@ private extension FeedbinAccountDelegate {
}
func refreshMissingArticles(_ account: Account, completion: @escaping (() -> Void)) {
os_log(.debug, log: log, "Refreshing missing articles...")
let articleIDs = Array(account.fetchArticleIDsForStatusesWithoutArticles())
let group = DispatchGroup()
let chunkedArticleIDs = articleIDs.chunked(into: 100)
refreshProgress.addToNumberOfTasks(chunkedArticleIDs.count - 1)
for chunk in chunkedArticleIDs {
group.enter()
caller.retrieveEntries(articleIDs: chunk) { [weak self] result in
switch result {
case .success(let entries):
self?.processEntries(account: account, entries: entries) {
self?.refreshProgress.completeTask()
group.leave()
}
case .failure(let error):
guard let self = self else { return }
os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription)
group.leave()
}
}
}
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done refreshing missing articles.")
completion()
}
}
func refreshArticles(_ account: Account, page: String?, completion: @escaping (() -> Void)) {
guard let page = page else {

View File

@ -0,0 +1,21 @@
//
// FeedbinImportResult.swift
// Account
//
// Created by Maurice Parker on 5/17/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct FeedbinImportResult: Codable {
let importResultID: Int
let complete: Bool
enum CodingKeys: String, CodingKey {
case importResultID = "id"
case complete
}
}

View File

@ -18,6 +18,8 @@ public enum LocalAccountDelegateError: String, Error {
final class LocalAccountDelegate: AccountDelegate {
let supportsSubFolders = false
let opmlImportInProgress = false
let server: String? = nil
var credentials: Credentials?
var accountMetadata: AccountMetadata?

View File

@ -112,6 +112,10 @@ public final class ArticlesDatabase {
return articlesTable.fetchStarredArticleIDs()
}
public func fetchArticleIDsForStatusesWithoutArticles() -> Set<String> {
return articlesTable.fetchArticleIDsForStatusesWithoutArticles()
}
public func mark(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<ArticleStatus>? {
return articlesTable.mark(articles, statusKey, flag)
}

View File

@ -307,6 +307,10 @@ final class ArticlesTable: DatabaseTable {
return statusesTable.fetchStarredArticleIDs()
}
func fetchArticleIDsForStatusesWithoutArticles() -> Set<String> {
return statusesTable.fetchArticleIDsForStatusesWithoutArticles()
}
func mark(_ articles: Set<Article>, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set<ArticleStatus>? {
return statusesTable.mark(articles.statuses(), statusKey, flag)

View File

@ -86,6 +86,10 @@ final class StatusesTable: DatabaseTable {
return fetchArticleIDs("select articleID from statuses where starred=1 and userDeleted=0;")
}
func fetchArticleIDsForStatusesWithoutArticles() -> Set<String> {
return fetchArticleIDs("select articleID from statuses s where userDeleted=0 and not exists (select 1 from articles a where a.articleID = s.articleID);")
}
func fetchArticleIDs(_ sql: String) -> Set<String> {
var statuses: Set<String>? = nil

View File

@ -26,6 +26,7 @@
5144EA43227A380F00D19003 /* ExportOPMLWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA42227A380F00D19003 /* ExportOPMLWindowController.swift */; };
5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */; };
5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */; };
51543685228F6753005E1CDF /* DetailAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51543684228F6753005E1CDF /* DetailAccountViewController.swift */; };
51554C24228B71910055115A /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; };
51554C25228B71910055115A /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
51554C30228B71A10055115A /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; };
@ -673,6 +674,7 @@
5144EA42227A380F00D19003 /* ExportOPMLWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportOPMLWindowController.swift; sourceTree = "<group>"; };
5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsFeedbinWindowController.swift; sourceTree = "<group>"; };
5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsFeedbin.xib; sourceTree = "<group>"; };
51543684228F6753005E1CDF /* DetailAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAccountViewController.swift; sourceTree = "<group>"; };
51554BFC228B6EB50055115A /* SyncDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SyncDatabase.xcodeproj; path = Frameworks/SyncDatabase/SyncDatabase.xcodeproj; sourceTree = SOURCE_ROOT; };
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = "<group>"; };
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = "<group>"; };
@ -1037,10 +1039,11 @@
5183CCEC22711DCE0010922C /* Settings.storyboard */,
51E595AA228DF94C00FCC42B /* SettingsTableViewCell.xib */,
5183CCEE227125970010922C /* SettingsViewController.swift */,
51E595AC228E1C2100FCC42B /* AddAccountViewController.swift */,
51F85BE6227245FC00C787DC /* AboutViewController.swift */,
51543684228F6753005E1CDF /* DetailAccountViewController.swift */,
51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */,
51EF0F7B2277919E0050506E /* TimelineNumberOfLinesViewController.swift */,
51E595AC228E1C2100FCC42B /* AddAccountViewController.swift */,
);
path = Settings;
sourceTree = "<group>";
@ -2347,6 +2350,7 @@
51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */,
51C452AE2265104D00C03939 /* TimelineStringFormatter.swift in Sources */,
512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */,
51543685228F6753005E1CDF /* DetailAccountViewController.swift in Sources */,
51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */,
51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */,
51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */,

View File

@ -177,7 +177,8 @@ class NavigationStateController {
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
@ -197,6 +198,10 @@ class NavigationStateController {
rebuildBackingStores()
}
@objc func accountStateDidChange(_ note: Notification) {
rebuildBackingStores()
}
@objc func userDefaultsDidChange(_ note: Notification) {
self.sortDirection = AppDefaults.timelineSortDirection
}
@ -230,6 +235,8 @@ class NavigationStateController {
func rebuildShadowTable() {
shadowTable = [[Node]]()
for i in 0..<treeController.rootNode.numberOfChildNodes {
var result = [Node]()
@ -245,7 +252,7 @@ class NavigationStateController {
}
}
shadowTable[i] = result
shadowTable.append(result)
}

View File

@ -0,0 +1,33 @@
//
// DetailAccountViewController.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 5/17/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
class DetailAccountViewController: UITableViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var activeSwitch: UISwitch!
weak var account: Account?
override func viewDidLoad() {
super.viewDidLoad()
guard let account = account else { return }
nameTextField.text = account.name
activeSwitch.isOn = account.isActive
}
override func viewWillDisappear(_ animated: Bool) {
account?.name = nameTextField.text
account?.isActive = activeSwitch.isOn
}
}

View File

@ -309,6 +309,99 @@
</objects>
<point key="canvasLocation" x="465" y="152"/>
</scene>
<!--Detail Account View Controller-->
<scene sceneID="TQp-8g-7td">
<objects>
<tableViewController storyboardIdentifier="DetailAccountViewController" id="SLc-SS-bhp" customClass="DetailAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="DfF-oG-mSd">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<sections>
<tableViewSection id="Zeb-b0-lsx">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="5DR-M4-NFv">
<rect key="frame" x="0.0" y="35" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5DR-M4-NFv" id="edh-bL-MIR">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="19" translatesAutoresizingMaskIntoConstraints="NO" id="XJG-0j-coE">
<rect key="frame" x="20" y="11.5" width="374" height="21"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oEe-a4-fph">
<rect key="frame" x="0.0" y="0.0" width="45" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="8Xs-17-Ubp">
<rect key="frame" x="64" y="0.0" width="310" height="21"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits"/>
</textField>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="XJG-0j-coE" firstAttribute="leading" secondItem="edh-bL-MIR" secondAttribute="leading" constant="20" id="2lj-H8-Iux"/>
<constraint firstItem="XJG-0j-coE" firstAttribute="centerY" secondItem="edh-bL-MIR" secondAttribute="centerY" id="ZDw-ch-uB6"/>
<constraint firstAttribute="trailing" secondItem="XJG-0j-coE" secondAttribute="trailing" constant="20" id="jih-VV-fBp"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="4n9-sW-i8D">
<rect key="frame" x="0.0" y="79" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="4n9-sW-i8D" id="h3v-g9-biw">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Active" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pvF-Ge-m4M">
<rect key="frame" x="20" y="11.5" width="48" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="J5R-kl-pYE">
<rect key="frame" x="345" y="6.5" width="51" height="31"/>
</switch>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="J5R-kl-pYE" secondAttribute="trailing" constant="20" id="0P4-Vq-dZp"/>
<constraint firstItem="pvF-Ge-m4M" firstAttribute="leading" secondItem="h3v-g9-biw" secondAttribute="leadingMargin" id="CIb-cr-t6V"/>
<constraint firstItem="J5R-kl-pYE" firstAttribute="centerY" secondItem="h3v-g9-biw" secondAttribute="centerY" id="Qrx-J1-99r"/>
<constraint firstItem="pvF-Ge-m4M" firstAttribute="centerY" secondItem="h3v-g9-biw" secondAttribute="centerY" id="Rq1-zD-1X7"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="VzJ-7V-Jfa">
<rect key="frame" x="0.0" y="123" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VzJ-7V-Jfa" id="GyY-75-Dmv">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="SLc-SS-bhp" id="PMF-j4-uum"/>
<outlet property="delegate" destination="SLc-SS-bhp" id="kha-OO-7FD"/>
</connections>
</tableView>
<connections>
<outlet property="activeSwitch" destination="J5R-kl-pYE" id="qEU-6p-G82"/>
<outlet property="nameTextField" destination="8Xs-17-Ubp" id="lKK-bI-mPR"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CQK-9Z-wvA" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1155" y="145"/>
</scene>
<!--Add Account View Controller-->
<scene sceneID="HbE-f2-Dbd">
<objects>
@ -401,7 +494,7 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kmn-Q7-rga" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1365.217391304348" y="160.04464285714286"/>
<point key="canvasLocation" x="1826" y="139"/>
</scene>
<!--Timeline Text-->
<scene sceneID="07z-Vb-4Fm">

View File

@ -48,6 +48,8 @@ class SettingsViewController: UITableViewController {
buildLabel.translatesAutoresizingMaskIntoConstraints = false
tableView.tableFooterView = buildLabel
tableView.reloadData()
}
// MARK: UITableView
@ -96,10 +98,12 @@ class SettingsViewController: UITableViewController {
case 0:
let sortedAccounts = AccountManager.shared.sortedAccounts
if indexPath.row == sortedAccounts.count {
let timeline = UIStoryboard.settings.instantiateController(ofType: AddAccountViewController.self)
self.navigationController?.pushViewController(timeline, animated: true)
let controller = UIStoryboard.settings.instantiateController(ofType: AddAccountViewController.self)
self.navigationController?.pushViewController(controller, animated: true)
} else {
// TODO
let controller = UIStoryboard.settings.instantiateController(ofType: DetailAccountViewController.self)
controller.account = sortedAccounts[indexPath.row]
self.navigationController?.pushViewController(controller, animated: true)
}
case 1:
switch indexPath.row {